@uzysjung/agent-harness 26.153.0 → 26.154.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
@@ -6,7 +6,6 @@ import {
6
6
  CLI_BASES,
7
7
  CONTINUOUS_SKILLS,
8
8
  DEFAULT_OPTIONS,
9
- DEV_METHOD_SKILL_IDS,
10
9
  EXTERNAL_ASSETS,
11
10
  HARNESS_ANCHOR_FILE,
12
11
  HARNESS_IMPORT_LINE,
@@ -19,6 +18,7 @@ import {
19
18
  RETIRED_SKILL_IDS,
20
19
  TRACKS,
21
20
  TRACK_AGENTS,
21
+ UPDATE_GROUPS,
22
22
  __commonJS,
23
23
  __toESM,
24
24
  anyTrack,
@@ -39,6 +39,7 @@ import {
39
39
  ensureDir,
40
40
  ensureProjectSkeleton,
41
41
  experimentalOptInCandidates,
42
+ extractFrontmatter,
42
43
  filterApplicableAssets,
43
44
  formatResidentCostLine,
44
45
  hasUiTrack,
@@ -51,12 +52,14 @@ import {
51
52
  isHarnessOwned,
52
53
  isInstallScope,
53
54
  isTrack,
55
+ landsOnDisk,
54
56
  legacyInstallLogPath,
55
57
  listFilesRecursive,
56
58
  mergeExternalFiles,
57
59
  readInstallLog,
58
60
  renderFillScaffold,
59
61
  residentCost,
62
+ resolveBundleRoot,
60
63
  resolveRules,
61
64
  resolveScope,
62
65
  stripHarnessImport,
@@ -64,7 +67,7 @@ import {
64
67
  upsertHarnessImport,
65
68
  withContinuousSkillsNote,
66
69
  writeInstallLog
67
- } from "./chunk-OYNSK6QF.js";
70
+ } from "./chunk-242TVGMZ.js";
68
71
 
69
72
  // node_modules/sisteransi/src/index.js
70
73
  var require_src = __commonJS({
@@ -749,7 +752,7 @@ var cac = (name = "") => new CAC(name);
749
752
  // package.json
750
753
  var package_default = {
751
754
  name: "@uzysjung/agent-harness",
752
- version: "26.153.0",
755
+ version: "26.154.0",
753
756
  description: "Curate vetted AI-coding skills & plugins by your tech stack \u2014 install only what you need, across Claude Code, Codex, OpenCode & Antigravity",
754
757
  type: "module",
755
758
  publishConfig: {
@@ -814,6 +817,8 @@ import { fileURLToPath } from "url";
814
817
 
815
818
  // src/baseline-targets.ts
816
819
  init_esm_shims();
820
+ import { existsSync, readFileSync } from "fs";
821
+ import { join } from "path";
817
822
  var BASELINE_KINDS = ["rules", "agents", "hooks", "skills"];
818
823
  var BASELINE_PREFIX = "baseline:";
819
824
  var PREFIX_BY_KIND = [
@@ -851,6 +856,42 @@ function isBaselineExcluded(target, excluded) {
851
856
  const t = classifyBaselineTarget(target);
852
857
  return t !== null && excluded.has(t.id);
853
858
  }
859
+ var HINT_MAX = 96;
860
+ function clip(text) {
861
+ const t = text.replace(/\s+/g, " ").trim();
862
+ return t.length > HINT_MAX ? `${t.slice(0, HINT_MAX - 1)}\u2026` : t;
863
+ }
864
+ function describeBaselineTarget(t, templatesDir = join(resolveBundleRoot(), "templates")) {
865
+ const file = t.kind === "skills" ? join(templatesDir, "skills", t.name, "SKILL.md") : join(templatesDir, t.kind, `${t.name}.${t.kind === "hooks" ? "sh" : "md"}`);
866
+ if (!existsSync(file)) return void 0;
867
+ const content = readFileSync(file, "utf8");
868
+ if (t.kind === "agents" || t.kind === "skills") {
869
+ const fm = extractFrontmatter(content) ?? "";
870
+ const m = /^description:\s*(.+)$/m.exec(fm);
871
+ const raw = m?.[1]?.trim().replace(/^["']|["']$/g, "");
872
+ return raw ? clip(raw) : void 0;
873
+ }
874
+ if (t.kind === "hooks") {
875
+ const lines = content.split("\n").slice(1);
876
+ const comment = [];
877
+ for (const l of lines) {
878
+ if (!l.startsWith("#")) break;
879
+ comment.push(l.replace(/^#\s?/, ""));
880
+ }
881
+ return comment.length > 0 ? clip(comment.join(" \xB7 ")) : void 0;
882
+ }
883
+ const body = content.split("\n");
884
+ const heading = body.find((l) => l.startsWith("# "))?.slice(2).trim();
885
+ const first = body.slice(body.findIndex((l) => l.startsWith("# ")) + 1).find((l) => l.trim() !== "")?.replace(/^[-*]\s+/, "").replace(/\*\*/g, "");
886
+ if (!heading) return void 0;
887
+ return clip(first ? `${heading} \xB7 ${first}` : heading);
888
+ }
889
+ function withBaselineHints(targets, templatesDir) {
890
+ return targets.map((t) => {
891
+ const hint = describeBaselineTarget(t, templatesDir);
892
+ return hint ? { ...t, hint } : t;
893
+ });
894
+ }
854
895
 
855
896
  // src/cli-targets.ts
856
897
  init_esm_shims();
@@ -988,18 +1029,18 @@ function padDisplay(s, width) {
988
1029
  init_esm_shims();
989
1030
  import {
990
1031
  chmodSync as chmodSync3,
991
- existsSync as existsSync13,
1032
+ existsSync as existsSync14,
992
1033
  mkdirSync as mkdirSync4,
993
1034
  readdirSync as readdirSync4,
994
- readFileSync as readFileSync12,
1035
+ readFileSync as readFileSync13,
995
1036
  writeFileSync as writeFileSync6
996
1037
  } from "fs";
997
- import { basename as basename5, dirname as dirname4, join as join12, resolve } from "path";
1038
+ import { basename as basename5, dirname as dirname4, join as join13, resolve } from "path";
998
1039
 
999
1040
  // src/ci-scaffold.ts
1000
1041
  init_esm_shims();
1001
- import { existsSync } from "fs";
1002
- import { join } from "path";
1042
+ import { existsSync as existsSync2 } from "fs";
1043
+ import { join as join2 } from "path";
1003
1044
  var PYTHON_TRACKS = "data|csr-fastapi|full";
1004
1045
  var VARIANTS = [
1005
1046
  {
@@ -1023,12 +1064,12 @@ function installCiScaffold(ctx) {
1023
1064
  for (const v2 of VARIANTS) {
1024
1065
  if (!v2.applies(ctx.tracks)) continue;
1025
1066
  const relTarget = `.github/workflows/${v2.target}`;
1026
- const target = join(ctx.projectDir, relTarget);
1027
- if (existsSync(target)) {
1067
+ const target = join2(ctx.projectDir, relTarget);
1068
+ if (existsSync2(target)) {
1028
1069
  report.skippedExisting.push(relTarget);
1029
1070
  continue;
1030
1071
  }
1031
- copyFile(join(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
1072
+ copyFile(join2(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
1032
1073
  report.written.push(relTarget);
1033
1074
  }
1034
1075
  return report;
@@ -1039,8 +1080,8 @@ init_esm_shims();
1039
1080
 
1040
1081
  // src/antigravity/transform.ts
1041
1082
  init_esm_shims();
1042
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1043
- import { basename, join as join5 } from "path";
1083
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
1084
+ import { basename, join as join6 } from "path";
1044
1085
 
1045
1086
  // src/codex/agents-md.ts
1046
1087
  init_esm_shims();
@@ -1055,8 +1096,8 @@ function renderAgentsMd(params) {
1055
1096
 
1056
1097
  // src/codex/skills.ts
1057
1098
  init_esm_shims();
1058
- import { chmodSync, existsSync as existsSync2, readFileSync } from "fs";
1059
- import { join as join2 } from "path";
1099
+ import { chmodSync, existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1100
+ import { join as join3 } from "path";
1060
1101
  function renderBundledSkill(source) {
1061
1102
  const trimmed = source.trimEnd();
1062
1103
  const lines = trimmed.split(/\r?\n/);
@@ -1088,14 +1129,14 @@ function writeBundledSkillDirs(params) {
1088
1129
  const { harnessRoot, projectDir, skillIds, writer } = params;
1089
1130
  const written = [];
1090
1131
  for (const id of skillIds) {
1091
- const srcDir = join2(harnessRoot, "templates/skills", id);
1092
- const srcSkill = join2(srcDir, "SKILL.md");
1093
- if (!existsSync2(srcSkill)) {
1132
+ const srcDir = join3(harnessRoot, "templates/skills", id);
1133
+ const srcSkill = join3(srcDir, "SKILL.md");
1134
+ if (!existsSync3(srcSkill)) {
1094
1135
  continue;
1095
1136
  }
1096
- const targetDir = join2(projectDir, ".agents", "skills", id);
1097
- const targetSkill = join2(targetDir, "SKILL.md");
1098
- if (!writer.write(targetSkill, renderBundledSkill(readFileSync(srcSkill, "utf8")))) {
1137
+ const targetDir = join3(projectDir, ".agents", "skills", id);
1138
+ const targetSkill = join3(targetDir, "SKILL.md");
1139
+ if (!writer.write(targetSkill, renderBundledSkill(readFileSync2(srcSkill, "utf8")))) {
1099
1140
  continue;
1100
1141
  }
1101
1142
  written.push(targetSkill);
@@ -1103,10 +1144,10 @@ function writeBundledSkillDirs(params) {
1103
1144
  if (rel === "SKILL.md") continue;
1104
1145
  if (rel.split("/").some((segment) => segment.startsWith("."))) continue;
1105
1146
  const segments = rel.split("/");
1106
- const raw = readFileSync(join2(srcDir, ...segments), "utf8");
1147
+ const raw = readFileSync2(join3(srcDir, ...segments), "utf8");
1107
1148
  const ported = rel.endsWith(".md") || rel.endsWith(".sh") ? `${portBody(raw)}
1108
1149
  ` : raw;
1109
- const target = join2(targetDir, ...segments);
1150
+ const target = join3(targetDir, ...segments);
1110
1151
  if (!writer.write(target, ported, { createInRefresh: true })) continue;
1111
1152
  if (rel.endsWith(".sh")) chmodSync(target, 493);
1112
1153
  written.push(target);
@@ -1117,13 +1158,13 @@ function writeBundledSkillDirs(params) {
1117
1158
 
1118
1159
  // src/owned-write.ts
1119
1160
  init_esm_shims();
1120
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
1161
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
1121
1162
  import { dirname, relative, sep } from "path";
1122
1163
 
1123
1164
  // src/foreign-slot.ts
1124
1165
  init_esm_shims();
1125
1166
  import { lstatSync } from "fs";
1126
- import { join as join3 } from "path";
1167
+ import { join as join4 } from "path";
1127
1168
  var SKILL_SLOT = /^((?:\.claude|\.agents)\/skills\/[^/]+)(?:\/|$)/;
1128
1169
  function occupiedByNonDirectory(path) {
1129
1170
  const stat = lstatSync(path, { throwIfNoEntry: false });
@@ -1132,15 +1173,15 @@ function occupiedByNonDirectory(path) {
1132
1173
  function foreignOwnedTarget(projectDir, rel) {
1133
1174
  const slot = SKILL_SLOT.exec(rel)?.[1];
1134
1175
  if (slot === void 0) return null;
1135
- if (occupiedByNonDirectory(join3(projectDir, slot))) return slot;
1176
+ if (occupiedByNonDirectory(join4(projectDir, slot))) return slot;
1136
1177
  if (rel !== slot) {
1137
1178
  const parts = rel.slice(slot.length + 1).split("/");
1138
1179
  let cursor = slot;
1139
1180
  for (const part of parts.slice(0, -1)) {
1140
1181
  cursor = `${cursor}/${part}`;
1141
- if (occupiedByNonDirectory(join3(projectDir, cursor))) return cursor;
1182
+ if (occupiedByNonDirectory(join4(projectDir, cursor))) return cursor;
1142
1183
  }
1143
- const leaf = lstatSync(join3(projectDir, rel), { throwIfNoEntry: false });
1184
+ const leaf = lstatSync(join4(projectDir, rel), { throwIfNoEntry: false });
1144
1185
  if (leaf !== void 0 && !leaf.isFile()) return rel;
1145
1186
  }
1146
1187
  return null;
@@ -1163,13 +1204,13 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
1163
1204
  return false;
1164
1205
  }
1165
1206
  const digest = hashContent(content);
1166
- if (!existsSync3(absPath)) {
1207
+ if (!existsSync4(absPath)) {
1167
1208
  if (refreshOnly && !opts?.createInRefresh) return false;
1168
1209
  mkdirSync(dirname(absPath), { recursive: true });
1169
1210
  writeFileSync(absPath, content);
1170
1211
  updated++;
1171
1212
  } else {
1172
- const current = readFileSync2(absPath, "utf8");
1213
+ const current = readFileSync3(absPath, "utf8");
1173
1214
  if (current !== content) {
1174
1215
  if (!isHarnessOwned(baseline, rel, current)) {
1175
1216
  backupPaths.push(backupFile(absPath, now));
@@ -1196,8 +1237,8 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
1196
1237
 
1197
1238
  // src/rules-port.ts
1198
1239
  init_esm_shims();
1199
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1200
- import { join as join4 } from "path";
1240
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1241
+ import { join as join5 } from "path";
1201
1242
  function stripClaudeFrontmatter(source) {
1202
1243
  if (!source.startsWith("---\n")) return source.trim();
1203
1244
  const end = source.indexOf("\n---", 4);
@@ -1207,9 +1248,9 @@ function stripClaudeFrontmatter(source) {
1207
1248
  function portRules(harnessRoot, ruleNames) {
1208
1249
  const ported = [];
1209
1250
  for (const name of ruleNames) {
1210
- const src = join4(harnessRoot, "templates/rules", `${name}.md`);
1211
- if (!existsSync4(src)) continue;
1212
- ported.push({ name, body: stripClaudeFrontmatter(readFileSync3(src, "utf8")) });
1251
+ const src = join5(harnessRoot, "templates/rules", `${name}.md`);
1252
+ if (!existsSync5(src)) continue;
1253
+ ported.push({ name, body: stripClaudeFrontmatter(readFileSync4(src, "utf8")) });
1213
1254
  }
1214
1255
  return ported;
1215
1256
  }
@@ -1233,7 +1274,7 @@ function runAntigravityTransform(params) {
1233
1274
  const antigravityInstalled = rulesFile !== null;
1234
1275
  const harnessRuleFiles = [];
1235
1276
  for (const rule of portRules(harnessRoot, rules)) {
1236
- const target = join5(projectDir, ".agents", "rules", `${rule.name}.md`);
1277
+ const target = join6(projectDir, ".agents", "rules", `${rule.name}.md`);
1237
1278
  if (!writer.write(target, `${rule.body}
1238
1279
  `, { createInRefresh: antigravityInstalled })) {
1239
1280
  continue;
@@ -1254,14 +1295,14 @@ function runAntigravityTransform(params) {
1254
1295
  };
1255
1296
  }
1256
1297
  function writeRules(harnessRoot, projectDir, writer, selectedInternalSkills) {
1257
- const claudeMdPath = join5(harnessRoot, "templates/CLAUDE.md");
1258
- const templatePath = join5(harnessRoot, "templates/antigravity/AGENTS.md.template");
1259
- if (!existsSync5(claudeMdPath) || !existsSync5(templatePath)) {
1298
+ const claudeMdPath = join6(harnessRoot, "templates/CLAUDE.md");
1299
+ const templatePath = join6(harnessRoot, "templates/antigravity/AGENTS.md.template");
1300
+ if (!existsSync6(claudeMdPath) || !existsSync6(templatePath)) {
1260
1301
  return null;
1261
1302
  }
1262
- const claudeMd = readFileSync4(claudeMdPath, "utf8");
1263
- const template = readFileSync4(templatePath, "utf8");
1264
- const target = join5(projectDir, ".agents", "rules", "uzys-harness.md");
1303
+ const claudeMd = readFileSync5(claudeMdPath, "utf8");
1304
+ const template = readFileSync5(templatePath, "utf8");
1305
+ const target = join6(projectDir, ".agents", "rules", "uzys-harness.md");
1265
1306
  const rulesOut = renderAgentsMd({
1266
1307
  template,
1267
1308
  claudeMd,
@@ -1278,18 +1319,18 @@ function writeRules(harnessRoot, projectDir, writer, selectedInternalSkills) {
1278
1319
  // src/codex/opt-in.ts
1279
1320
  init_esm_shims();
1280
1321
  import { homedir } from "os";
1281
- import { join as join6 } from "path";
1322
+ import { join as join7 } from "path";
1282
1323
 
1283
1324
  // src/codex/trust-entry.ts
1284
1325
  init_esm_shims();
1285
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
1326
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
1286
1327
  import { dirname as dirname2 } from "path";
1287
1328
  var TRUST_BLOCK_REGEX = /\[projects\."([^"]+)"\]/g;
1288
1329
  function registerTrustEntry(opts) {
1289
1330
  const { configPath, projectDir } = opts;
1290
1331
  try {
1291
1332
  mkdirSync2(dirname2(configPath), { recursive: true });
1292
- const existing = existsSync6(configPath) ? readFileSync5(configPath, "utf8") : "";
1333
+ const existing = existsSync7(configPath) ? readFileSync6(configPath, "utf8") : "";
1293
1334
  if (hasTrustEntry(existing, projectDir)) {
1294
1335
  return { status: "already-present" };
1295
1336
  }
@@ -1313,8 +1354,8 @@ function hasTrustEntry(configContent, projectDir) {
1313
1354
 
1314
1355
  // src/codex/opt-in.ts
1315
1356
  function runCodexOptIn(ctx) {
1316
- const codexHome = ctx.codexHome ?? join6(homedir(), ".codex");
1317
- const configPath = join6(codexHome, "config.toml");
1357
+ const codexHome = ctx.codexHome ?? join7(homedir(), ".codex");
1358
+ const configPath = join7(codexHome, "config.toml");
1318
1359
  const result = registerTrustEntry({ configPath, projectDir: ctx.projectDir });
1319
1360
  return {
1320
1361
  trustEntry: {
@@ -1327,8 +1368,8 @@ function runCodexOptIn(ctx) {
1327
1368
 
1328
1369
  // src/codex/transform.ts
1329
1370
  init_esm_shims();
1330
- import { chmodSync as chmodSync2, existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1331
- import { basename as basename2, join as join7 } from "path";
1371
+ import { chmodSync as chmodSync2, existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
1372
+ import { basename as basename2, join as join8 } from "path";
1332
1373
 
1333
1374
  // src/codex/config-toml.ts
1334
1375
  init_esm_shims();
@@ -1405,12 +1446,12 @@ function runCodexTransform(params) {
1405
1446
  refreshOnly
1406
1447
  } = params;
1407
1448
  const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
1408
- const claudeMd = readRequired(join7(harnessRoot, "templates/CLAUDE.md"));
1409
- const agentsTemplate = readRequired(join7(harnessRoot, "templates/codex/AGENTS.md.template"));
1410
- const configTemplate = readRequired(join7(harnessRoot, "templates/codex/config.toml.template"));
1449
+ const claudeMd = readRequired(join8(harnessRoot, "templates/CLAUDE.md"));
1450
+ const agentsTemplate = readRequired(join8(harnessRoot, "templates/codex/AGENTS.md.template"));
1451
+ const configTemplate = readRequired(join8(harnessRoot, "templates/codex/config.toml.template"));
1411
1452
  const projectName = basename2(projectDir);
1412
- const mcp = readOptionalJson(join7(harnessRoot, ".mcp.json"));
1413
- const agentsMdPath = join7(projectDir, "AGENTS.md");
1453
+ const mcp = readOptionalJson(join8(harnessRoot, ".mcp.json"));
1454
+ const agentsMdPath = join8(projectDir, "AGENTS.md");
1414
1455
  ensureDir(projectDir);
1415
1456
  const agentsMdOut = renderAgentsMd({
1416
1457
  template: agentsTemplate,
@@ -1425,7 +1466,7 @@ function runCodexTransform(params) {
1425
1466
  harnessRules: renderRulesBlock(portRules(harnessRoot, rules))
1426
1467
  });
1427
1468
  writer.write(agentsMdPath, agentsMdOut);
1428
- const configTomlPath = join7(projectDir, ".codex/config.toml");
1469
+ const configTomlPath = join8(projectDir, ".codex/config.toml");
1429
1470
  writer.write(
1430
1471
  configTomlPath,
1431
1472
  renderConfigToml({
@@ -1435,15 +1476,15 @@ function runCodexTransform(params) {
1435
1476
  mcp
1436
1477
  })
1437
1478
  );
1438
- const hookDir = join7(projectDir, ".codex/hooks");
1479
+ const hookDir = join8(projectDir, ".codex/hooks");
1439
1480
  const hookFiles = [];
1440
1481
  for (const hook of HOOK_NAMES) {
1441
- const src = join7(harnessRoot, "templates/hooks", `${hook}.sh`);
1442
- if (!existsSync7(src)) {
1482
+ const src = join8(harnessRoot, "templates/hooks", `${hook}.sh`);
1483
+ if (!existsSync8(src)) {
1443
1484
  continue;
1444
1485
  }
1445
- const ported = readFileSync6(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
1446
- const target = join7(hookDir, `${hook}.sh`);
1486
+ const ported = readFileSync7(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
1487
+ const target = join8(hookDir, `${hook}.sh`);
1447
1488
  if (!writer.write(target, ported)) continue;
1448
1489
  chmodSync2(target, 493);
1449
1490
  hookFiles.push(target);
@@ -1463,17 +1504,17 @@ function runCodexTransform(params) {
1463
1504
  };
1464
1505
  }
1465
1506
  function readRequired(path) {
1466
- if (!existsSync7(path)) {
1507
+ if (!existsSync8(path)) {
1467
1508
  throw new Error(`Codex transform: required source missing: ${path}`);
1468
1509
  }
1469
- return readFileSync6(path, "utf8");
1510
+ return readFileSync7(path, "utf8");
1470
1511
  }
1471
1512
  function readOptionalJson(path) {
1472
- if (!existsSync7(path)) {
1513
+ if (!existsSync8(path)) {
1473
1514
  return null;
1474
1515
  }
1475
1516
  try {
1476
- return JSON.parse(readFileSync6(path, "utf8"));
1517
+ return JSON.parse(readFileSync7(path, "utf8"));
1477
1518
  } catch {
1478
1519
  return null;
1479
1520
  }
@@ -1481,8 +1522,8 @@ function readOptionalJson(path) {
1481
1522
 
1482
1523
  // src/opencode/transform.ts
1483
1524
  init_esm_shims();
1484
- import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync7, unlinkSync } from "fs";
1485
- import { basename as basename3, join as join8 } from "path";
1525
+ import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync8, unlinkSync } from "fs";
1526
+ import { basename as basename3, join as join9 } from "path";
1486
1527
 
1487
1528
  // src/opencode/agents-md.ts
1488
1529
  init_esm_shims();
@@ -1525,15 +1566,15 @@ function runOpencodeTransform(params) {
1525
1566
  refreshOnly
1526
1567
  } = params;
1527
1568
  const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
1528
- const claudeMd = readRequired2(join8(harnessRoot, "templates/CLAUDE.md"));
1529
- const agentsTemplate = readRequired2(join8(harnessRoot, "templates/opencode/AGENTS.md.template"));
1569
+ const claudeMd = readRequired2(join9(harnessRoot, "templates/CLAUDE.md"));
1570
+ const agentsTemplate = readRequired2(join9(harnessRoot, "templates/opencode/AGENTS.md.template"));
1530
1571
  const opencodeTemplate = readRequired2(
1531
- join8(harnessRoot, "templates/opencode/opencode.json.template")
1572
+ join9(harnessRoot, "templates/opencode/opencode.json.template")
1532
1573
  );
1533
1574
  const projectName = basename3(projectDir);
1534
- const mcp = readOptionalJson2(join8(harnessRoot, ".mcp.json"));
1575
+ const mcp = readOptionalJson2(join9(harnessRoot, ".mcp.json"));
1535
1576
  ensureDir(projectDir);
1536
- const agentsMdPath = join8(projectDir, "AGENTS.md");
1577
+ const agentsMdPath = join9(projectDir, "AGENTS.md");
1537
1578
  const agentsMdOut = renderAgentsMd2({
1538
1579
  template: agentsTemplate,
1539
1580
  claudeMd,
@@ -1549,7 +1590,7 @@ function runOpencodeTransform(params) {
1549
1590
  harnessRules: renderRulesBlock(portRules(harnessRoot, rules))
1550
1591
  });
1551
1592
  writer.write(agentsMdPath, agentsMdOut);
1552
- const opencodeJsonPath = join8(projectDir, "opencode.json");
1593
+ const opencodeJsonPath = join9(projectDir, "opencode.json");
1553
1594
  writer.write(opencodeJsonPath, renderOpencodeJson({ template: opencodeTemplate, mcp }));
1554
1595
  const skillFiles = writeBundledSkillDirs({
1555
1596
  harnessRoot,
@@ -1558,13 +1599,13 @@ function runOpencodeTransform(params) {
1558
1599
  writer
1559
1600
  });
1560
1601
  const retiredCommands = [];
1561
- const cmdDir = join8(projectDir, ".opencode/commands");
1562
- if (existsSync8(cmdDir)) {
1602
+ const cmdDir = join9(projectDir, ".opencode/commands");
1603
+ if (existsSync9(cmdDir)) {
1563
1604
  for (const entry of readdirSync(cmdDir)) {
1564
1605
  if (!entry.endsWith(".md")) continue;
1565
1606
  const id = entry.slice(0, -3);
1566
- if (!existsSync8(join8(harnessRoot, "templates/skills", id, "SKILL.md"))) continue;
1567
- const victim = join8(cmdDir, entry);
1607
+ if (!existsSync9(join9(harnessRoot, "templates/skills", id, "SKILL.md"))) continue;
1608
+ const victim = join9(cmdDir, entry);
1568
1609
  try {
1569
1610
  backupFile(victim);
1570
1611
  unlinkSync(victim);
@@ -1582,17 +1623,17 @@ function runOpencodeTransform(params) {
1582
1623
  };
1583
1624
  }
1584
1625
  function readRequired2(path) {
1585
- if (!existsSync8(path)) {
1626
+ if (!existsSync9(path)) {
1586
1627
  throw new Error(`OpenCode transform: required source missing: ${path}`);
1587
1628
  }
1588
- return readFileSync7(path, "utf8");
1629
+ return readFileSync8(path, "utf8");
1589
1630
  }
1590
1631
  function readOptionalJson2(path) {
1591
- if (!existsSync8(path)) {
1632
+ if (!existsSync9(path)) {
1592
1633
  return null;
1593
1634
  }
1594
1635
  try {
1595
- return JSON.parse(readFileSync7(path, "utf8"));
1636
+ return JSON.parse(readFileSync8(path, "utf8"));
1596
1637
  } catch {
1597
1638
  return null;
1598
1639
  }
@@ -1684,8 +1725,8 @@ function runCliTransforms(params) {
1684
1725
 
1685
1726
  // src/env-files.ts
1686
1727
  init_esm_shims();
1687
- import { appendFileSync, existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
1688
- import { join as join9 } from "path";
1728
+ import { appendFileSync, existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
1729
+ import { join as join10 } from "path";
1689
1730
  var ENV_EXAMPLE_BODY = `# .env.example \u2014 csr-supabase Track
1690
1731
  # Copy to .env (gitignored) and fill in values: cp .env.example .env
1691
1732
 
@@ -1722,19 +1763,19 @@ function writeEnvExample(projectDir, tracks) {
1722
1763
  if (!tracks.some((t) => ENV_EXAMPLE_TRACKS.includes(t))) {
1723
1764
  return false;
1724
1765
  }
1725
- const path = join9(projectDir, ".env.example");
1726
- if (existsSync9(path)) {
1766
+ const path = join10(projectDir, ".env.example");
1767
+ if (existsSync10(path)) {
1727
1768
  return false;
1728
1769
  }
1729
1770
  writeFileSync3(path, ENV_EXAMPLE_BODY);
1730
1771
  return true;
1731
1772
  }
1732
1773
  function addGitignoreEnv(projectDir) {
1733
- const path = join9(projectDir, ".gitignore");
1734
- if (!existsSync9(path)) {
1774
+ const path = join10(projectDir, ".gitignore");
1775
+ if (!existsSync10(path)) {
1735
1776
  return false;
1736
1777
  }
1737
- const content = readFileSync8(path, "utf8");
1778
+ const content = readFileSync9(path, "utf8");
1738
1779
  if (GITIGNORE_ENV_PATTERN.test(content)) {
1739
1780
  return false;
1740
1781
  }
@@ -1748,11 +1789,11 @@ function addGitignoreEnv(projectDir) {
1748
1789
  var AGENT_ARTIFACT_DIRS = [".factory/", ".goose/", ".uzys-agent-harness/"];
1749
1790
  var GITIGNORE_AGENT_ARTIFACT_HEADER = "# agent CLI / harness \uC790\uB3D9 \uC0DD\uC131\uBB3C (auto-added by agent-harness)";
1750
1791
  function addGitignoreAgentArtifacts(projectDir) {
1751
- const path = join9(projectDir, ".gitignore");
1752
- if (!existsSync9(path)) {
1792
+ const path = join10(projectDir, ".gitignore");
1793
+ if (!existsSync10(path)) {
1753
1794
  return [];
1754
1795
  }
1755
- const content = readFileSync8(path, "utf8");
1796
+ const content = readFileSync9(path, "utf8");
1756
1797
  const missing = AGENT_ARTIFACT_DIRS.filter((pattern) => {
1757
1798
  const lineRegex = new RegExp(`^${pattern.replace(/\./g, "\\.").replace(/\//g, "/")}\\s*$`, "m");
1758
1799
  return !lineRegex.test(content);
@@ -1771,10 +1812,14 @@ ${block}
1771
1812
  // src/external-installer.ts
1772
1813
  init_esm_shims();
1773
1814
  import { spawnSync } from "child_process";
1774
- import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync9 } from "fs";
1815
+ import { existsSync as existsSync11, readdirSync as readdirSync2, readFileSync as readFileSync10 } from "fs";
1775
1816
  import { homedir as homedir2 } from "os";
1776
- import { join as join10 } from "path";
1817
+ import { join as join11 } from "path";
1777
1818
  var DEFAULT_SPAWN_TIMEOUT_MS = 12e4;
1819
+ var NPM_SPAWN_TIMEOUT_MS = 6e5;
1820
+ function spawnTimeoutFor(kind) {
1821
+ return kind === "npm" || kind === "npx-run" ? NPM_SPAWN_TIMEOUT_MS : DEFAULT_SPAWN_TIMEOUT_MS;
1822
+ }
1778
1823
  function selectExternalTargets(assets, ctx) {
1779
1824
  const conditionPassed = filterApplicableAssets(assets, ctx).filter(
1780
1825
  (a) => a.method.kind !== "internal"
@@ -1849,8 +1894,8 @@ function installOne(asset, ctx) {
1849
1894
  cwd
1850
1895
  );
1851
1896
  case "shell-script": {
1852
- const scriptPath = join10(ctx.harnessRoot, method.script);
1853
- if (!existsSync10(scriptPath)) {
1897
+ const scriptPath = join11(ctx.harnessRoot, method.script);
1898
+ if (!existsSync11(scriptPath)) {
1854
1899
  return {
1855
1900
  asset,
1856
1901
  ok: false,
@@ -1946,8 +1991,17 @@ function installPlugin(asset, spawn, method, scope, cwd) {
1946
1991
  );
1947
1992
  }
1948
1993
  function runSpawn(asset, spawn, cmd, args, cwd) {
1949
- const result = spawn(cmd, args, spawnOpts(cwd));
1994
+ const timeout = spawnTimeoutFor(asset.method.kind);
1995
+ const result = spawn(cmd, args, spawnOpts(cwd, timeout));
1950
1996
  if (result.error) {
1997
+ const code = result.error.code;
1998
+ if (code === "ETIMEDOUT") {
1999
+ return {
2000
+ asset,
2001
+ ok: false,
2002
+ message: `timed out after ${Math.round(timeout / 1e3)}s \u2014 finish it yourself: ${cmd} ${args.join(" ")}`
2003
+ };
2004
+ }
1951
2005
  return { asset, ok: false, message: result.error.message };
1952
2006
  }
1953
2007
  if ((result.status ?? 1) !== 0) {
@@ -1961,11 +2015,11 @@ function runSpawn(asset, spawn, cmd, args, cwd) {
1961
2015
  }
1962
2016
  return { asset, ok: true };
1963
2017
  }
1964
- function spawnOpts(cwd) {
2018
+ function spawnOpts(cwd, timeout = DEFAULT_SPAWN_TIMEOUT_MS) {
1965
2019
  return {
1966
2020
  encoding: "utf8",
1967
2021
  stdio: "pipe",
1968
- timeout: DEFAULT_SPAWN_TIMEOUT_MS,
2022
+ timeout,
1969
2023
  ...cwd ? { cwd } : {}
1970
2024
  };
1971
2025
  }
@@ -1980,17 +2034,17 @@ function detectVersion(method, spawn) {
1980
2034
  if (at2 <= 0) return void 0;
1981
2035
  const plugin = method.pluginId.slice(0, at2);
1982
2036
  const marketplaceShort = method.pluginId.slice(at2 + 1);
1983
- const cacheBase = join10(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
1984
- if (!existsSync10(cacheBase)) return void 0;
2037
+ const cacheBase = join11(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
2038
+ if (!existsSync11(cacheBase)) return void 0;
1985
2039
  const versions = readdirSync2(cacheBase).filter((v2) => /^\d/.test(v2)).sort();
1986
2040
  return versions.at(-1);
1987
2041
  }
1988
2042
  case "npm": {
1989
2043
  const npmRoot = getNpmGlobalRoot(spawn);
1990
2044
  if (!npmRoot) return void 0;
1991
- const pkgJson = join10(npmRoot, method.pkg, "package.json");
1992
- if (!existsSync10(pkgJson)) return void 0;
1993
- const parsed = JSON.parse(readFileSync9(pkgJson, "utf8"));
2045
+ const pkgJson = join11(npmRoot, method.pkg, "package.json");
2046
+ if (!existsSync11(pkgJson)) return void 0;
2047
+ const parsed = JSON.parse(readFileSync10(pkgJson, "utf8"));
1994
2048
  return parsed.version;
1995
2049
  }
1996
2050
  default:
@@ -2017,7 +2071,7 @@ function getNpmGlobalRoot(spawn) {
2017
2071
 
2018
2072
  // src/mcp-merge.ts
2019
2073
  init_esm_shims();
2020
- import { existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "fs";
2074
+ import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "fs";
2021
2075
  function parseTrackMcpMap(raw) {
2022
2076
  const rows = [];
2023
2077
  for (const line of raw.split(/\r?\n/)) {
@@ -2068,15 +2122,15 @@ function mergeMcpServers(base, rows, tracks) {
2068
2122
  return out;
2069
2123
  }
2070
2124
  function composeMcpJson(opts) {
2071
- const base = JSON.parse(readFileSync10(opts.templateMcpPath, "utf8"));
2072
- const merged = opts.existingPath && existsSync11(opts.existingPath) ? mergeUserBase(base, opts.existingPath) : base;
2073
- const mapRaw = existsSync11(opts.trackMapPath) ? readFileSync10(opts.trackMapPath, "utf8") : "";
2125
+ const base = JSON.parse(readFileSync11(opts.templateMcpPath, "utf8"));
2126
+ const merged = opts.existingPath && existsSync12(opts.existingPath) ? mergeUserBase(base, opts.existingPath) : base;
2127
+ const mapRaw = existsSync12(opts.trackMapPath) ? readFileSync11(opts.trackMapPath, "utf8") : "";
2074
2128
  const rows = parseTrackMcpMap(mapRaw);
2075
2129
  return mergeMcpServers(merged, rows, opts.tracks);
2076
2130
  }
2077
2131
  function mergeUserBase(base, existingPath) {
2078
2132
  try {
2079
- const existing = JSON.parse(readFileSync10(existingPath, "utf8"));
2133
+ const existing = JSON.parse(readFileSync11(existingPath, "utf8"));
2080
2134
  return {
2081
2135
  ...base,
2082
2136
  mcpServers: { ...base.mcpServers, ...existing.mcpServers }
@@ -2094,37 +2148,52 @@ function writeMcpJson(path, mcp) {
2094
2148
  init_esm_shims();
2095
2149
  import {
2096
2150
  copyFileSync,
2097
- existsSync as existsSync12,
2151
+ existsSync as existsSync13,
2098
2152
  mkdirSync as mkdirSync3,
2099
2153
  readdirSync as readdirSync3,
2100
- readFileSync as readFileSync11,
2154
+ readFileSync as readFileSync12,
2155
+ rmdirSync,
2156
+ statSync,
2101
2157
  unlinkSync as unlinkSync2,
2102
2158
  writeFileSync as writeFileSync5
2103
2159
  } from "fs";
2104
- import { basename as basename4, dirname as dirname3, join as join11 } from "path";
2160
+ import { basename as basename4, dirname as dirname3, join as join12 } from "path";
2105
2161
  var LEGACY_ANCHOR_FILE = ".claude/CLAUDE.md";
2106
- function buildUpdateSpec(projectDir, tracks) {
2162
+ function buildUpdateSpec(projectDir, tracks, only) {
2107
2163
  return {
2108
2164
  tracks: [...tracks],
2109
2165
  options: DEFAULT_OPTIONS,
2110
2166
  cli: ["claude"],
2111
2167
  projectDir,
2112
- scope: readInstallLog(projectDir)?.scope ?? "project"
2168
+ scope: readInstallLog(projectDir)?.scope ?? "project",
2169
+ // 전부 골랐으면 "제한 없음"과 같다 — 화면·기록에 제한이 있었던 것처럼 남기지 않는다.
2170
+ ...only !== void 0 && only.length > 0 && only.length < UPDATE_GROUPS.length ? { updateOnly: [...only] } : {}
2113
2171
  };
2114
2172
  }
2115
- function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}) {
2116
- const claudeDir = join11(projectDir, ".claude");
2173
+ function parseUpdateOnly(raw) {
2174
+ const invalid = raw.filter((v2) => !UPDATE_GROUPS.includes(v2));
2175
+ if (invalid.length > 0) return { ok: false, invalid };
2176
+ return { ok: true, groups: [...new Set(raw)] };
2177
+ }
2178
+ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
2179
+ const claudeDir = join12(projectDir, ".claude");
2180
+ const startedAt = Date.now();
2181
+ const wants = (g) => only === void 0 || only.includes(g);
2117
2182
  const report = {
2118
2183
  updated: {},
2119
2184
  pruned: {},
2120
2185
  staleHookRefs: [],
2121
2186
  claudeMdUpdated: false,
2187
+ backups: [],
2188
+ skippedGroups: only === void 0 ? [] : UPDATE_GROUPS.filter((g) => !only.includes(g)),
2189
+ anchorBackedUp: false,
2122
2190
  anchorCreated: false,
2123
2191
  rootImportAdded: false,
2124
2192
  rootBlockRefreshed: false,
2125
2193
  legacyAnchor: null,
2126
2194
  skillsBackedUp: [],
2127
2195
  skillsSkippedLinks: [],
2196
+ skillsPruned: [],
2128
2197
  policyBackedUp: [],
2129
2198
  externalUpdated: 0,
2130
2199
  externalBackedUp: [],
@@ -2140,14 +2209,15 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}) {
2140
2209
  demotedAgents: [],
2141
2210
  mcpAllowlistRetired: null
2142
2211
  };
2143
- const fresh = installNewAssets(projectDir, templatesDir);
2212
+ const fresh = installNewAssets(projectDir, templatesDir, wants);
2144
2213
  report.installedNew = fresh.installed;
2145
2214
  report.restored = fresh.restored;
2146
2215
  report.needsReinstall = fresh.needsReinstall;
2147
2216
  const policyBase = policyBaseline(projectDir);
2148
2217
  for (const { dir, ext } of POLICY_DIRS) {
2149
- const target = join11(claudeDir, dir);
2150
- const source = join11(templatesDir, dir);
2218
+ if (!wants(dir === "hooks" ? "hooks" : "rules")) continue;
2219
+ const target = join12(claudeDir, dir);
2220
+ const source = join12(templatesDir, dir);
2151
2221
  const label = `.claude/${dir}`;
2152
2222
  const ctx = { prefix: dir, baseline: policyBase };
2153
2223
  const synced = updateDir(target, source, ext, ctx);
@@ -2155,30 +2225,38 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}) {
2155
2225
  report.policyBackedUp.push(...synced.backedUp);
2156
2226
  report.pruned[label] = pruneOrphans(target, source, ext, ctx);
2157
2227
  }
2158
- refreshPolicyBaseline(projectDir, templatesDir);
2159
- const skillSync = syncSkills(
2160
- join11(claudeDir, "skills"),
2161
- join11(templatesDir, "skills"),
2228
+ const syncedDirs = POLICY_DIRS.filter((d2) => wants(d2.dir === "hooks" ? "hooks" : "rules")).map(
2229
+ (d2) => d2.dir
2230
+ );
2231
+ if (syncedDirs.length > 0) refreshPolicyBaseline(projectDir, templatesDir, syncedDirs);
2232
+ const skillSync = wants("skills") ? syncSkills(
2233
+ join12(claudeDir, "skills"),
2234
+ join12(templatesDir, "skills"),
2162
2235
  skillBaseline(projectDir),
2163
2236
  /* @__PURE__ */ new Date(),
2164
2237
  (relInSkills) => foreignOwnedTarget(projectDir, `.claude/skills/${relInSkills}`)
2165
- );
2166
- report.updated[".claude/skills"] = skillSync.updated;
2238
+ ) : { updated: 0, backedUp: [], skippedLinks: [], foreignOwned: [], pruned: [] };
2239
+ if (wants("skills")) report.updated[".claude/skills"] = skillSync.updated;
2167
2240
  report.skillsBackedUp = skillSync.backedUp;
2168
2241
  report.skillsSkippedLinks = skillSync.skippedLinks;
2169
- refreshSkillBaseline(projectDir);
2242
+ report.skillsPruned = skillSync.pruned;
2243
+ const newSkills = wants("new-skills") ? installNewSkillDirs(projectDir, templatesDir, installedTracks(projectDir)) : { installed: [], foreignOwned: [] };
2244
+ report.installedNew.push(...newSkills.installed);
2245
+ if (wants("skills")) refreshSkillBaseline(projectDir, templatesDir);
2246
+ else if (newSkills.installed.length > 0)
2247
+ recordNewSkillBaseline(projectDir, templatesDir, newSkills.installed);
2170
2248
  const installLog = readInstallLog(projectDir);
2171
- if (existsSync12(claudeDir) && (installLog === null || installLog.spec.cli.includes("claude"))) {
2172
- syncHarnessAnchor(projectDir, templatesDir, report);
2249
+ if (existsSync13(claudeDir) && (installLog === null || installLog.spec.cli.includes("claude"))) {
2250
+ if (wants("anchor")) syncHarnessAnchor(projectDir, templatesDir, report);
2173
2251
  }
2174
- const settingsPath = join11(claudeDir, "settings.json");
2175
- if (existsSync12(settingsPath)) {
2252
+ const settingsPath = join12(claudeDir, "settings.json");
2253
+ if (wants("hooks") && existsSync13(settingsPath)) {
2176
2254
  report.staleHookRefs = cleanStaleHookRefs(settingsPath, claudeDir);
2177
2255
  }
2178
- report.mcpAllowlistRetired = retireMcpAllowlist(projectDir);
2256
+ if (wants("hooks")) report.mcpAllowlistRetired = retireMcpAllowlist(projectDir);
2179
2257
  report.retiredAgents = staleAgentFiles(claudeDir);
2180
2258
  report.demotedAgents = demotedAgentFiles(claudeDir, installedTracks(projectDir));
2181
- const external = refreshExternalCli(projectDir, harnessRoot);
2259
+ const external = wants("external") ? refreshExternalCli(projectDir, harnessRoot) : { externalUpdated: 0, externalBackedUp: [], externalForeignOwned: [] };
2182
2260
  report.externalUpdated = external.externalUpdated;
2183
2261
  report.externalBackedUp = external.externalBackedUp;
2184
2262
  const saidBySlotRow = new Set(report.skillsSkippedLinks.map((id) => `.claude/skills/${id}`));
@@ -2186,12 +2264,13 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}) {
2186
2264
  for (const f of [
2187
2265
  ...skillSync.foreignOwned,
2188
2266
  ...fresh.foreignOwned,
2267
+ ...newSkills.foreignOwned,
2189
2268
  ...external.externalForeignOwned
2190
2269
  ]) {
2191
2270
  if (!saidBySlotRow.has(f) && !merged.includes(f)) merged.push(f);
2192
2271
  }
2193
2272
  report.foreignOwned = merged;
2194
- const skillRefresh = (deps.refreshSkills ?? refreshExternalSkills)(projectDir);
2273
+ const skillRefresh = wants("external") ? (deps.refreshSkills ?? refreshExternalSkills)(projectDir) : { attempted: 0, refreshed: 0, failed: [], notInCatalog: [], unknown: false };
2195
2274
  report.externalSkillsRefreshed = skillRefresh.refreshed;
2196
2275
  report.externalSkillsFailed = skillRefresh.failed;
2197
2276
  report.externalSkillsNotInCatalog = [
@@ -2199,29 +2278,31 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}) {
2199
2278
  ...staleSkillDirs(claudeDir).filter((id) => !skillRefresh.notInCatalog.includes(id))
2200
2279
  ];
2201
2280
  report.externalSkillsUnknown = skillRefresh.unknown;
2281
+ report.backups = collectRunBackups(projectDir, startedAt);
2282
+ writeBackupList(projectDir, report.backups);
2202
2283
  return report;
2203
2284
  }
2204
2285
  function staleSkillDirs(claudeDir) {
2205
- const dir = join11(claudeDir, "skills");
2206
- if (!existsSync12(dir)) return [];
2286
+ const dir = join12(claudeDir, "skills");
2287
+ if (!existsSync13(dir)) return [];
2207
2288
  const stale = (id) => RENAMED_SKILL_IDS[id] !== void 0 || RETIRED_SKILL_IDS.includes(id);
2208
2289
  return readdirSync3(dir, { withFileTypes: true }).filter((e2) => e2.isDirectory() && stale(e2.name)).map((e2) => e2.name);
2209
2290
  }
2210
2291
  function staleAgentFiles(claudeDir) {
2211
- const dir = join11(claudeDir, "agents");
2212
- if (!existsSync12(dir)) return [];
2292
+ const dir = join12(claudeDir, "agents");
2293
+ if (!existsSync13(dir)) return [];
2213
2294
  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));
2214
2295
  }
2215
2296
  function demotedAgentFiles(claudeDir, tracks) {
2216
2297
  if (tracks.length === 0) return [];
2217
- const dir = join11(claudeDir, "agents");
2218
- if (!existsSync12(dir)) return [];
2298
+ const dir = join12(claudeDir, "agents");
2299
+ if (!existsSync13(dir)) return [];
2219
2300
  const demoted = TRACK_AGENTS.filter(([, pattern]) => !anyTrack(tracks, pattern)).map(
2220
2301
  ([id]) => id
2221
2302
  );
2222
2303
  return readdirSync3(dir, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".md")).map((e2) => basename4(e2.name, ".md")).filter((id) => demoted.includes(id));
2223
2304
  }
2224
- function installNewAssets(projectDir, templatesDir) {
2305
+ function installNewAssets(projectDir, templatesDir, wants = () => true) {
2225
2306
  const installed = [];
2226
2307
  const restored = [];
2227
2308
  const needsReinstall = [];
@@ -2231,17 +2312,19 @@ function installNewAssets(projectDir, templatesDir) {
2231
2312
  const baselineExcluded = new Set(log?.spec.baselineExclude ?? []);
2232
2313
  const priorBaseline = policyBaseline(projectDir);
2233
2314
  for (const entry of trackOnlyFileAssets(installedTracks(projectDir))) {
2315
+ const group = entry.target.startsWith(".claude/hooks/") || entry.target === ".claude/settings.json" ? "hooks" : "rules";
2316
+ if (!wants(group)) continue;
2234
2317
  if (entry.target === HARNESS_ANCHOR_FILE) continue;
2235
2318
  if (isBaselineExcluded(entry.target, baselineExcluded)) continue;
2236
- const target = join11(projectDir, entry.target);
2319
+ const target = join12(projectDir, entry.target);
2237
2320
  const foreign = foreignOwnedTarget(projectDir, entry.target);
2238
2321
  if (foreign !== null) {
2239
2322
  if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
2240
2323
  continue;
2241
2324
  }
2242
- if (existsSync12(target)) continue;
2243
- const source = join11(templatesDir, entry.source);
2244
- if (!existsSync12(source)) continue;
2325
+ if (existsSync13(target)) continue;
2326
+ const source = join12(templatesDir, entry.source);
2327
+ if (!existsSync13(source)) continue;
2245
2328
  if (entry.target.startsWith(".claude/")) {
2246
2329
  if (!claudeSelected) continue;
2247
2330
  if (entry.target.startsWith(".claude/hooks/") || entry.target === ".claude/settings.json") {
@@ -2257,6 +2340,77 @@ function installNewAssets(projectDir, templatesDir) {
2257
2340
  }
2258
2341
  return { installed, restored, needsReinstall, foreignOwned };
2259
2342
  }
2343
+ var BACKUP_SUFFIX = /\.backup-\d{8}T\d{6}(-\d+)?$/;
2344
+ var BACKUP_SCAN_DIRS = [".claude", ".codex", ".opencode", ".agents"];
2345
+ function collectRunBackups(projectDir, startedAt) {
2346
+ const out = [];
2347
+ const consider = (rel) => {
2348
+ if (!BACKUP_SUFFIX.test(rel)) return;
2349
+ const abs = join12(projectDir, rel);
2350
+ if (statSync(abs).mtimeMs < startedAt - 1e3) return;
2351
+ out.push({ path: rel.replace(BACKUP_SUFFIX, ""), backup: rel });
2352
+ };
2353
+ for (const e2 of readdirSync3(projectDir, { withFileTypes: true }))
2354
+ if (e2.isFile()) consider(e2.name);
2355
+ for (const dir of BACKUP_SCAN_DIRS) {
2356
+ for (const rel of listFilesRecursive(join12(projectDir, dir))) consider(`${dir}/${rel}`);
2357
+ }
2358
+ return out.sort((a, b2) => a.path.localeCompare(b2.path));
2359
+ }
2360
+ function writeBackupList(projectDir, backups) {
2361
+ const dir = join12(projectDir, ".uzys-agent-harness");
2362
+ const file = join12(dir, "update-backups.json");
2363
+ try {
2364
+ if (backups.length === 0) {
2365
+ if (existsSync13(file)) unlinkSync2(file);
2366
+ return;
2367
+ }
2368
+ if (!existsSync13(dir)) return;
2369
+ writeFileSync5(
2370
+ file,
2371
+ `${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)}
2372
+ `
2373
+ );
2374
+ } catch {
2375
+ }
2376
+ }
2377
+ function installNewSkillDirs(projectDir, templatesDir, tracks) {
2378
+ const installed = [];
2379
+ const foreignOwned = [];
2380
+ const log = readInstallLog(projectDir);
2381
+ const excluded = new Set(log?.spec.baselineExclude ?? []);
2382
+ if (!(log?.spec.cli ?? ["claude"]).includes("claude")) return { installed, foreignOwned };
2383
+ const spec = buildAssetSpec({ tracks, options: DEFAULT_OPTIONS });
2384
+ for (const entry of buildManifest(spec)) {
2385
+ if (entry.type !== "dir" || !entry.target.startsWith(".claude/skills/")) continue;
2386
+ if (!entry.applies(spec) || isBaselineExcluded(entry.target, excluded)) continue;
2387
+ const target = join12(projectDir, entry.target);
2388
+ const foreign = foreignOwnedTarget(projectDir, entry.target);
2389
+ if (foreign !== null) {
2390
+ if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
2391
+ continue;
2392
+ }
2393
+ if (existsSync13(target)) continue;
2394
+ const source = join12(templatesDir, entry.source);
2395
+ if (!existsSync13(source)) continue;
2396
+ copyDir(source, target, (rel) => foreignOwnedTarget(projectDir, `${entry.target}/${rel}`));
2397
+ installed.push(entry.target);
2398
+ }
2399
+ return { installed, foreignOwned };
2400
+ }
2401
+ function recordNewSkillBaseline(projectDir, templatesDir, targets) {
2402
+ const log = readInstallLog(projectDir);
2403
+ if (!log) return;
2404
+ const ids = new Set(targets.map((t) => t.slice(".claude/skills/".length)));
2405
+ const added = collectSkillHashes(projectDir, templatesDir).filter(
2406
+ (f) => ids.has(f.path.split("/")[0] ?? "")
2407
+ );
2408
+ const kept = (log.skillFiles ?? []).filter((f) => !ids.has(f.path.split("/")[0] ?? ""));
2409
+ try {
2410
+ writeInstallLog(projectDir, { ...log, skillFiles: [...kept, ...added] });
2411
+ } catch {
2412
+ }
2413
+ }
2260
2414
  function trackOnlyFileAssets(tracks) {
2261
2415
  const base = { tracks, selectedInternalSkills: [] };
2262
2416
  const optedOut = { ...base, withEcc: false, withTauri: false };
@@ -2265,34 +2419,45 @@ function trackOnlyFileAssets(tracks) {
2265
2419
  (e2) => e2.type === "file" && e2.applies(optedOut) && e2.applies(optedIn)
2266
2420
  );
2267
2421
  }
2268
- function syncHarnessAnchor(projectDir, templatesDir, report) {
2269
- if (existsSync12(join11(projectDir, LEGACY_ANCHOR_FILE))) {
2422
+ function syncHarnessAnchor(projectDir, templatesDir, report, now = /* @__PURE__ */ new Date()) {
2423
+ if (existsSync13(join12(projectDir, LEGACY_ANCHOR_FILE))) {
2270
2424
  report.legacyAnchor = LEGACY_ANCHOR_FILE;
2271
2425
  }
2272
- const templateMd = join11(templatesDir, "CLAUDE.md");
2273
- if (!existsSync12(templateMd)) return;
2274
- const anchor = join11(projectDir, HARNESS_ANCHOR_FILE);
2275
- const existed = existsSync12(anchor);
2276
- copyFileSync(templateMd, anchor);
2426
+ const templateMd = join12(templatesDir, "CLAUDE.md");
2427
+ if (!existsSync13(templateMd)) return;
2428
+ const anchor = join12(projectDir, HARNESS_ANCHOR_FILE);
2429
+ const existed = existsSync13(anchor);
2277
2430
  if (existed) {
2431
+ const current = readFileSync12(anchor, "utf8");
2432
+ if (current !== readFileSync12(templateMd, "utf8")) {
2433
+ const recorded = readInstallLog(projectDir)?.templates.rootClaudeMd;
2434
+ const owned = recorded?.path === HARNESS_ANCHOR_FILE && hashContent(current) === recorded.sha256;
2435
+ if (!owned) {
2436
+ backupFile(anchor, now);
2437
+ report.anchorBackedUp = true;
2438
+ }
2439
+ copyFileSync(templateMd, anchor);
2440
+ }
2278
2441
  report.claudeMdUpdated = true;
2442
+ recordAnchorBaseline(projectDir, anchor);
2279
2443
  report.rootBlockRefreshed = upsertRootImport(projectDir);
2280
2444
  return;
2281
2445
  }
2446
+ copyFileSync(templateMd, anchor);
2282
2447
  report.anchorCreated = true;
2283
2448
  report.rootImportAdded = upsertRootImport(projectDir);
2284
2449
  recordAnchorBaseline(projectDir, anchor);
2285
2450
  }
2286
2451
  function upsertRootImport(projectDir) {
2287
- const target = join11(projectDir, "CLAUDE.md");
2288
- const existing = existsSync12(target) ? readFileSync11(target, "utf8") : null;
2452
+ const target = join12(projectDir, "CLAUDE.md");
2453
+ const existing = existsSync13(target) ? readFileSync12(target, "utf8") : null;
2289
2454
  const next = upsertHarnessImport(existing, {
2290
2455
  projectName: basename4(projectDir),
2291
2456
  tracks: installedTracks(projectDir),
2292
2457
  // ADR-085 — update 는 선택 목록을 인자로 받지 않는다. `syncSkills` 와 같은 판정을 쓴다:
2293
2458
  // `.claude/skills/<id>` 가 있으면 그 스킬은 이 설치가 고른 것이다.
2294
2459
  continuousSkills: CONTINUOUS_SKILLS.map((s) => s.id).filter(
2295
- (id) => existsSync12(join11(projectDir, ".claude/skills", id))
2460
+ (id) => existsSync13(join12(projectDir, ".claude/skills", id))
2296
2461
  )
2297
2462
  });
2298
2463
  if (next === existing) return false;
@@ -2312,7 +2477,7 @@ function recordAnchorBaseline(projectDir, anchor) {
2312
2477
  ...log.templates,
2313
2478
  rootClaudeMd: {
2314
2479
  path: HARNESS_ANCHOR_FILE,
2315
- sha256: hashContent(readFileSync11(anchor, "utf8"))
2480
+ sha256: hashContent(readFileSync12(anchor, "utf8"))
2316
2481
  }
2317
2482
  }
2318
2483
  };
@@ -2354,16 +2519,16 @@ function isHarnessOwned2(ctx, rel, current) {
2354
2519
  return isHarnessOwned(ctx.baseline, `${ctx.prefix}/${rel}`, current);
2355
2520
  }
2356
2521
  function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2357
- if (!existsSync12(target) || !existsSync12(source)) return { updated: 0, backedUp: [] };
2522
+ if (!existsSync13(target) || !existsSync13(source)) return { updated: 0, backedUp: [] };
2358
2523
  let updated = 0;
2359
2524
  const backedUp = [];
2360
2525
  for (const file of readdirSync3(target)) {
2361
2526
  if (!file.endsWith(ext)) continue;
2362
- const targetFile = join11(target, file);
2363
- const sourceFile = join11(source, file);
2364
- if (!existsSync12(sourceFile)) continue;
2365
- const next = readFileSync11(sourceFile, "utf8");
2366
- const current = readFileSync11(targetFile, "utf8");
2527
+ const targetFile = join12(target, file);
2528
+ const sourceFile = join12(source, file);
2529
+ if (!existsSync13(sourceFile)) continue;
2530
+ const next = readFileSync12(sourceFile, "utf8");
2531
+ const current = readFileSync12(targetFile, "utf8");
2367
2532
  if (current === next) continue;
2368
2533
  if (!isHarnessOwned2(ctx, file, current)) {
2369
2534
  backupFile(targetFile, now);
@@ -2375,35 +2540,36 @@ function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2375
2540
  return { updated, backedUp };
2376
2541
  }
2377
2542
  function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Date(), foreignOf) {
2378
- if (!existsSync12(targetDir) || !existsSync12(sourceDir))
2379
- return { updated: 0, backedUp: [], skippedLinks: [], foreignOwned: [] };
2543
+ if (!existsSync13(targetDir) || !existsSync13(sourceDir))
2544
+ return { updated: 0, backedUp: [], skippedLinks: [], foreignOwned: [], pruned: [] };
2380
2545
  let updated = 0;
2381
2546
  const backedUp = [];
2382
2547
  const skippedLinks = [];
2383
2548
  const foreignOwned = [];
2549
+ const pruned = [];
2384
2550
  for (const skill of readdirSync3(sourceDir, { withFileTypes: true })) {
2385
2551
  if (!skill.isDirectory()) continue;
2386
- const targetSkill = join11(targetDir, skill.name);
2552
+ const targetSkill = join12(targetDir, skill.name);
2387
2553
  if (occupiedByNonDirectory(targetSkill)) {
2388
2554
  skippedLinks.push(skill.name);
2389
2555
  continue;
2390
2556
  }
2391
- if (!existsSync12(targetSkill)) continue;
2392
- for (const rel of listFilesRecursive(join11(sourceDir, skill.name))) {
2393
- const targetFile = join11(targetSkill, rel);
2557
+ if (!existsSync13(targetSkill)) continue;
2558
+ for (const rel of listFilesRecursive(join12(sourceDir, skill.name))) {
2559
+ const targetFile = join12(targetSkill, rel);
2394
2560
  const foreign = foreignOf(`${skill.name}/${rel}`);
2395
2561
  if (foreign !== null) {
2396
2562
  if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
2397
2563
  continue;
2398
2564
  }
2399
- const next = readFileSync11(join11(sourceDir, skill.name, rel), "utf8");
2400
- if (!existsSync12(targetFile)) {
2565
+ const next = readFileSync12(join12(sourceDir, skill.name, rel), "utf8");
2566
+ if (!existsSync13(targetFile)) {
2401
2567
  mkdirSync3(dirname3(targetFile), { recursive: true });
2402
2568
  writeFileSync5(targetFile, next);
2403
2569
  updated++;
2404
2570
  continue;
2405
2571
  }
2406
- const current = readFileSync11(targetFile, "utf8");
2572
+ const current = readFileSync12(targetFile, "utf8");
2407
2573
  if (current === next) continue;
2408
2574
  const recorded = baseline.get(`${skill.name}/${rel}`);
2409
2575
  if (recorded === void 0 || hashContent(current) !== recorded) {
@@ -2413,8 +2579,23 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
2413
2579
  writeFileSync5(targetFile, next);
2414
2580
  updated++;
2415
2581
  }
2582
+ const bundled = new Set(listFilesRecursive(join12(sourceDir, skill.name)));
2583
+ for (const rel of listFilesRecursive(targetSkill)) {
2584
+ if (bundled.has(rel) || basename4(rel).includes(".backup-")) continue;
2585
+ if (foreignOf(`${skill.name}/${rel}`) !== null) continue;
2586
+ const targetFile = join12(targetSkill, rel);
2587
+ const recorded = baseline.get(`${skill.name}/${rel}`);
2588
+ if (recorded === void 0 || hashContent(readFileSync12(targetFile, "utf8")) !== recorded) {
2589
+ backupFile(targetFile, now);
2590
+ backedUp.push(`${skill.name}/${rel}`);
2591
+ }
2592
+ unlinkSync2(targetFile);
2593
+ pruned.push(`${skill.name}/${rel}`);
2594
+ for (let d2 = dirname3(targetFile); d2 !== targetSkill && readdirSync3(d2).length === 0; d2 = dirname3(d2))
2595
+ rmdirSync(d2);
2596
+ }
2416
2597
  }
2417
- return { updated, backedUp, skippedLinks, foreignOwned };
2598
+ return { updated, backedUp, skippedLinks, foreignOwned, pruned };
2418
2599
  }
2419
2600
  function skillBaseline(projectDir) {
2420
2601
  const log = readInstallLog(projectDir);
@@ -2424,10 +2605,14 @@ function policyBaseline(projectDir) {
2424
2605
  const log = readInstallLog(projectDir);
2425
2606
  return new Map((log?.policyFiles ?? []).map((f) => [f.path, f.sha256]));
2426
2607
  }
2427
- function refreshPolicyBaseline(projectDir, templatesDir) {
2608
+ function refreshPolicyBaseline(projectDir, templatesDir, dirs) {
2428
2609
  const log = readInstallLog(projectDir);
2429
2610
  if (!log) return;
2430
- const policyFiles = collectPolicyHashes(projectDir, templatesDir);
2611
+ const inDirs = (path) => dirs === void 0 || dirs.some((d2) => path.startsWith(`${d2}/`));
2612
+ const policyFiles = [
2613
+ ...(log.policyFiles ?? []).filter((f) => !inDirs(f.path)),
2614
+ ...collectPolicyHashes(projectDir, templatesDir).filter((f) => inDirs(f.path))
2615
+ ];
2431
2616
  const next = { ...log };
2432
2617
  if (policyFiles.length > 0) next.policyFiles = policyFiles;
2433
2618
  else delete next.policyFiles;
@@ -2436,10 +2621,10 @@ function refreshPolicyBaseline(projectDir, templatesDir) {
2436
2621
  } catch {
2437
2622
  }
2438
2623
  }
2439
- function refreshSkillBaseline(projectDir) {
2624
+ function refreshSkillBaseline(projectDir, templatesDir) {
2440
2625
  const log = readInstallLog(projectDir);
2441
2626
  if (!log) return;
2442
- const skillFiles = collectSkillHashes(projectDir);
2627
+ const skillFiles = collectSkillHashes(projectDir, templatesDir);
2443
2628
  const next = { ...log };
2444
2629
  if (skillFiles.length > 0) next.skillFiles = skillFiles;
2445
2630
  else delete next.skillFiles;
@@ -2449,16 +2634,16 @@ function refreshSkillBaseline(projectDir) {
2449
2634
  }
2450
2635
  }
2451
2636
  function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2452
- if (!existsSync12(target) || !existsSync12(source)) return [];
2637
+ if (!existsSync13(target) || !existsSync13(source)) return [];
2453
2638
  const removed = [];
2454
2639
  for (const file of readdirSync3(target)) {
2455
2640
  if (!file.endsWith(ext)) continue;
2456
- const sourceFile = join11(source, file);
2457
- if (!existsSync12(sourceFile)) {
2458
- const targetFile = join11(target, file);
2641
+ const sourceFile = join12(source, file);
2642
+ if (!existsSync13(sourceFile)) {
2643
+ const targetFile = join12(target, file);
2459
2644
  if (!ctx.baseline.has(`${ctx.prefix}/${file}`)) continue;
2460
2645
  try {
2461
- const current = readFileSync11(targetFile, "utf8");
2646
+ const current = readFileSync12(targetFile, "utf8");
2462
2647
  if (!isHarnessOwned2(ctx, file, current)) backupFile(targetFile, now);
2463
2648
  unlinkSync2(targetFile);
2464
2649
  removed.push(file);
@@ -2469,8 +2654,8 @@ function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()
2469
2654
  return removed;
2470
2655
  }
2471
2656
  function retireMcpAllowlist(projectDir, now = /* @__PURE__ */ new Date()) {
2472
- const target = join11(projectDir, ".mcp-allowlist");
2473
- if (!existsSync12(target)) return null;
2657
+ const target = join12(projectDir, ".mcp-allowlist");
2658
+ if (!existsSync13(target)) return null;
2474
2659
  try {
2475
2660
  const backup = backupFile(target, now);
2476
2661
  unlinkSync2(target);
@@ -2482,7 +2667,7 @@ function retireMcpAllowlist(projectDir, now = /* @__PURE__ */ new Date()) {
2482
2667
  function cleanStaleHookRefs(settingsPath, claudeDir) {
2483
2668
  let settings;
2484
2669
  try {
2485
- settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
2670
+ settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
2486
2671
  } catch {
2487
2672
  return [];
2488
2673
  }
@@ -2509,7 +2694,7 @@ function cleanStaleHookRefs(settingsPath, claudeDir) {
2509
2694
  function keepHookRef(hook, claudeDir, removed) {
2510
2695
  const relPath = projectAnchoredRef(hook?.command ?? "", claudeDir);
2511
2696
  if (relPath === void 0) return true;
2512
- const exists = existsSync12(join11(claudeDir, relPath));
2697
+ const exists = existsSync13(join12(claudeDir, relPath));
2513
2698
  if (!exists && !removed.includes(relPath)) removed.push(relPath);
2514
2699
  return exists;
2515
2700
  }
@@ -2536,14 +2721,14 @@ function projectAnchoredRef(command, claudeDir) {
2536
2721
  function runInstall(ctx) {
2537
2722
  const { harnessRoot, projectDir, spec } = ctx;
2538
2723
  const mode = ctx.mode ?? "fresh";
2539
- const templatesDir = join12(harnessRoot, "templates");
2540
- if (!existsSync13(templatesDir)) {
2724
+ const templatesDir = join13(harnessRoot, "templates");
2725
+ if (!existsSync14(templatesDir)) {
2541
2726
  throw new Error(`Templates dir not found: ${templatesDir}`);
2542
2727
  }
2543
- const claudeDir = join12(projectDir, ".claude");
2728
+ const claudeDir = join13(projectDir, ".claude");
2544
2729
  const previousLog = readInstallLog(projectDir);
2545
2730
  const claudeWasSelected = previousLog?.spec.cli.includes("claude") ?? false;
2546
- if (mode === "update" && !existsSync13(claudeDir) && (previousLog === null || claudeWasSelected)) {
2731
+ if (mode === "update" && !existsSync14(claudeDir) && (previousLog === null || claudeWasSelected)) {
2547
2732
  throw new Error(
2548
2733
  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}`
2549
2734
  );
@@ -2628,9 +2813,9 @@ function runInstall(ctx) {
2628
2813
  }
2629
2814
  function healStaleHookRefs(spec, projectDir) {
2630
2815
  if (!spec.cli.includes("claude")) return [];
2631
- const settingsPath = join12(projectDir, ".claude/settings.json");
2632
- if (!existsSync13(settingsPath)) return [];
2633
- return cleanStaleHookRefs(settingsPath, join12(projectDir, ".claude"));
2816
+ const settingsPath = join13(projectDir, ".claude/settings.json");
2817
+ if (!existsSync14(settingsPath)) return [];
2818
+ return cleanStaleHookRefs(settingsPath, join13(projectDir, ".claude"));
2634
2819
  }
2635
2820
  function resolveBackupPath(ctx, mode, claudeDir) {
2636
2821
  const wantBackup = ctx.backup ?? (mode === "update" || mode === "reinstall");
@@ -2638,7 +2823,13 @@ function resolveBackupPath(ctx, mode, claudeDir) {
2638
2823
  return mode === "update" ? copyBackupDir(claudeDir) : backupDir(claudeDir);
2639
2824
  }
2640
2825
  function runUpdateInstall(ctx, templatesDir, backupPath) {
2641
- const updateReport = runUpdateMode(ctx.projectDir, templatesDir, ctx.harnessRoot);
2826
+ const updateReport = runUpdateMode(
2827
+ ctx.projectDir,
2828
+ templatesDir,
2829
+ ctx.harnessRoot,
2830
+ {},
2831
+ ctx.spec.updateOnly
2832
+ );
2642
2833
  const baseline = {
2643
2834
  filesCopied: 0,
2644
2835
  dirsCopied: 0,
@@ -2696,12 +2887,12 @@ function installCliNeutralAssets(manifestSpec, projectDir, templatesDir, baselin
2696
2887
  continue;
2697
2888
  }
2698
2889
  if (!isCliNeutralTarget(entry.target)) continue;
2699
- const source = join12(templatesDir, entry.source);
2700
- if (!existsSync13(source)) {
2890
+ const source = join13(templatesDir, entry.source);
2891
+ if (!existsSync14(source)) {
2701
2892
  result.skipped += 1;
2702
2893
  continue;
2703
2894
  }
2704
- copyFile(source, join12(projectDir, entry.target));
2895
+ copyFile(source, join13(projectDir, entry.target));
2705
2896
  result.filesCopied += 1;
2706
2897
  }
2707
2898
  return result;
@@ -2713,9 +2904,9 @@ function backupEditedPolicyFile(entryTarget, target, source, baseline) {
2713
2904
  if (!POLICY_DIRS.some(({ dir, ext }) => rel.startsWith(`${dir}/`) && rel.endsWith(ext))) {
2714
2905
  return null;
2715
2906
  }
2716
- if (!existsSync13(target)) return null;
2717
- const current = readFileSync12(target, "utf-8");
2718
- if (current === readFileSync12(source, "utf-8")) return null;
2907
+ if (!existsSync14(target)) return null;
2908
+ const current = readFileSync13(target, "utf-8");
2909
+ if (current === readFileSync13(source, "utf-8")) return null;
2719
2910
  const recorded = baseline.get(rel);
2720
2911
  if (recorded !== void 0 && recorded === hashContent(current)) return null;
2721
2912
  return backupFile(target);
@@ -2728,14 +2919,14 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
2728
2919
  if (!entry.applies(manifestSpec)) {
2729
2920
  continue;
2730
2921
  }
2731
- const target = join12(projectDir, entry.target);
2922
+ const target = join13(projectDir, entry.target);
2732
2923
  if (isBaselineExcluded(entry.target, baselineExcluded)) {
2733
2924
  result.excluded.push(entry.target);
2734
- if (existsSync13(target)) result.excludedOnDisk.push(entry.target);
2925
+ if (existsSync14(target)) result.excludedOnDisk.push(entry.target);
2735
2926
  continue;
2736
2927
  }
2737
- const source = join12(templatesDir, entry.source);
2738
- if (!existsSync13(source)) {
2928
+ const source = join13(templatesDir, entry.source);
2929
+ if (!existsSync14(source)) {
2739
2930
  result.skipped += 1;
2740
2931
  continue;
2741
2932
  }
@@ -2746,7 +2937,7 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
2746
2937
  }
2747
2938
  if (entry.type === "file") {
2748
2939
  if (entry.target === ".claude/settings.json") {
2749
- const backup = backupFileIfChanged(target, readFileSync12(source, "utf-8"));
2940
+ const backup = backupFileIfChanged(target, readFileSync13(source, "utf-8"));
2750
2941
  if (backup) {
2751
2942
  result.backups.push(backup);
2752
2943
  }
@@ -2770,8 +2961,8 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
2770
2961
  }
2771
2962
  accumulateCategory(result.categories, entry);
2772
2963
  }
2773
- const hookDir = join12(projectDir, ".claude/hooks");
2774
- if (existsSync13(hookDir)) {
2964
+ const hookDir = join13(projectDir, ".claude/hooks");
2965
+ if (existsSync14(hookDir)) {
2775
2966
  chmodHooksSync(hookDir);
2776
2967
  }
2777
2968
  writeInstalledTracks(projectDir, manifestSpec.tracks);
@@ -2785,9 +2976,9 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
2785
2976
  return result;
2786
2977
  }
2787
2978
  function harnessAnchorLog(projectDir) {
2788
- const anchor = join12(projectDir, HARNESS_ANCHOR_FILE);
2789
- if (!existsSync13(anchor)) return null;
2790
- return { path: HARNESS_ANCHOR_FILE, sha256: hashContent(readFileSync12(anchor, "utf-8")) };
2979
+ const anchor = join13(projectDir, HARNESS_ANCHOR_FILE);
2980
+ if (!existsSync14(anchor)) return null;
2981
+ return { path: HARNESS_ANCHOR_FILE, sha256: hashContent(readFileSync13(anchor, "utf-8")) };
2791
2982
  }
2792
2983
  function writeEnvironmentFiles(projectDir, tracks) {
2793
2984
  return {
@@ -2845,8 +3036,8 @@ function writeInstallLogSafe(ctx, externalFiles, external, rootClaudeMdLog, prev
2845
3036
  claudeDirMovedAside,
2846
3037
  rootFiles
2847
3038
  );
2848
- const skillFiles = collectSkillHashes(ctx.projectDir);
2849
- const policyFiles = collectPolicyHashes(ctx.projectDir, join12(ctx.harnessRoot, "templates"));
3039
+ const skillFiles = collectSkillHashes(ctx.projectDir, join13(ctx.harnessRoot, "templates"));
3040
+ const policyFiles = collectPolicyHashes(ctx.projectDir, join13(ctx.harnessRoot, "templates"));
2850
3041
  const merged = mergeExternalFiles(ctx.projectDir, previousLog?.externalFiles, externalFiles);
2851
3042
  writeInstallLog(ctx.projectDir, {
2852
3043
  ...log,
@@ -2862,11 +3053,11 @@ function writeInstallLogSafe(ctx, externalFiles, external, rootClaudeMdLog, prev
2862
3053
  }
2863
3054
  }
2864
3055
  function composeAndWriteMcp(harnessRoot, projectDir, spec) {
2865
- const mcpPath = join12(projectDir, ".mcp.json");
2866
- const created = !existsSync13(mcpPath);
3056
+ const mcpPath = join13(projectDir, ".mcp.json");
3057
+ const created = !existsSync14(mcpPath);
2867
3058
  const composed = composeMcpJson({
2868
- templateMcpPath: join12(harnessRoot, "templates/mcp.json"),
2869
- trackMapPath: join12(harnessRoot, "templates/track-mcp-map.tsv"),
3059
+ templateMcpPath: join13(harnessRoot, "templates/mcp.json"),
3060
+ trackMapPath: join13(harnessRoot, "templates/track-mcp-map.tsv"),
2870
3061
  existingPath: mcpPath,
2871
3062
  tracks: spec.tracks
2872
3063
  });
@@ -2919,15 +3110,15 @@ function accumulateCategory(cats, entry) {
2919
3110
  }
2920
3111
  }
2921
3112
  function writeInstalledTracks(projectDir, tracks) {
2922
- const path = join12(projectDir, ".claude/.installed-tracks");
3113
+ const path = join13(projectDir, ".claude/.installed-tracks");
2923
3114
  mkdirSync4(dirname4(path), { recursive: true });
2924
3115
  const sorted = [...new Set(tracks)].sort().join("\n");
2925
3116
  writeFileSync6(path, `${sorted}
2926
3117
  `);
2927
3118
  }
2928
3119
  function writeRootClaudeMd(projectDir, tracks, continuousSkills) {
2929
- const target = join12(projectDir, "CLAUDE.md");
2930
- const existing = existsSync13(target) ? readFileSync12(target, "utf-8") : null;
3120
+ const target = join13(projectDir, "CLAUDE.md");
3121
+ const existing = existsSync14(target) ? readFileSync13(target, "utf-8") : null;
2931
3122
  const content = upsertHarnessImport(existing, {
2932
3123
  projectName: basename5(projectDir),
2933
3124
  tracks,
@@ -2952,8 +3143,8 @@ function listHookFiles(hookDir) {
2952
3143
 
2953
3144
  // src/commands/install-render.ts
2954
3145
  init_esm_shims();
2955
- import { existsSync as existsSync14, readdirSync as readdirSync5 } from "fs";
2956
- import { join as join13 } from "path";
3146
+ import { existsSync as existsSync15, readdirSync as readdirSync5 } from "fs";
3147
+ import { join as join14 } from "path";
2957
3148
 
2958
3149
  // src/preset-recommend.ts
2959
3150
  init_esm_shims();
@@ -3022,7 +3213,11 @@ function renderInstallHeader(log, spec, mode) {
3022
3213
  if (mode !== "update") {
3023
3214
  const assetSpec = buildManifestSpec(spec);
3024
3215
  const cost = formatResidentCostLine(
3025
- residentCost(buildManifest(assetSpec).filter((e2) => e2.applies(assetSpec))),
3216
+ residentCost(
3217
+ buildManifest(assetSpec).filter(
3218
+ (e2) => e2.applies(assetSpec) && landsOnDisk(e2.target, spec.cli)
3219
+ )
3220
+ ),
3026
3221
  summarizeContextCost(finalAssets).unmeasuredCount
3027
3222
  );
3028
3223
  if (cost) log(` ${c.dim(`\xB7 ${cost}`)}`);
@@ -3071,6 +3266,10 @@ function createInstallRenderer(log, spec, verbose) {
3071
3266
  log(` ${c.bold(`\u2501\u2501 ${CATEGORY_TITLES[asset.category]} \u2501\u2501`)}`);
3072
3267
  currentCategory = asset.category;
3073
3268
  }
3269
+ if (asset.method.kind === "npm" || asset.method.kind === "npx-run") {
3270
+ const what = asset.method.kind === "npm" ? `npm install ${asset.method.pkg}@${asset.method.version}` : `npx ${asset.method.cmd}@${asset.method.version}`;
3271
+ log(` ${c.dim(`\u2026 ${asset.id} ${what} \u2014 running, may take a few minutes`)}`);
3272
+ }
3074
3273
  },
3075
3274
  onAssetResult: (result) => {
3076
3275
  const meta = result.ok ? formatAssetMeta(result.asset, result.version) : result.message ?? "failed";
@@ -3095,18 +3294,33 @@ function renderUpdateSummary(log, spec, report) {
3095
3294
  summarizeContextCost(finalSelectedAssets(spec.tracks, spec.userOverride)).unmeasuredCount
3096
3295
  );
3097
3296
  if (cost) log(infoRow("CONTEXT", cost));
3297
+ const backups = report.updateMode?.backups ?? [];
3298
+ if (backups.length > 0) {
3299
+ log(
3300
+ infoRow(
3301
+ "BACKUPS",
3302
+ `${backups.length} edited file(s) kept as *.backup-<time> \xB7 list: .uzys-agent-harness/update-backups.json`
3303
+ )
3304
+ );
3305
+ log(
3306
+ infoRow(
3307
+ "NEXT",
3308
+ 'to re-apply your edits on the new version, ask audit-harness-fit: "update \uBC31\uC5C5\uBCF8\uC758 \uB0B4 \uD3B8\uC9D1\uC744 \uC0C8 \uD310\uC5D0 \uB2E4\uC2DC \uC5B9\uC5B4\uC918"'
3309
+ )
3310
+ );
3311
+ }
3098
3312
  log("");
3099
3313
  }
3100
3314
  function residentEntriesOnDisk(spec) {
3101
3315
  const assetSpec = buildManifestSpec(spec);
3102
3316
  const planned = buildManifest(assetSpec).filter((e2) => e2.applies(assetSpec));
3103
- const agentsDir = join13(spec.projectDir, ".claude", "agents");
3104
- const onDisk = existsSync14(agentsDir) ? readdirSync5(agentsDir, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".md")).map((e2) => ({
3317
+ const agentsDir = join14(spec.projectDir, ".claude", "agents");
3318
+ const onDisk = existsSync15(agentsDir) ? readdirSync5(agentsDir, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".md")).map((e2) => ({
3105
3319
  source: `agents/${e2.name}`,
3106
3320
  target: `.claude/agents/${e2.name}`,
3107
3321
  // 배포판이 아니라 **이 프로젝트의 파일**을 잰다 — 사용자가 고친 descriptor 도,
3108
3322
  // 배포판에 더는 없는 에이전트도 실제로 상주하는 것은 이쪽이다.
3109
- file: join13(agentsDir, e2.name)
3323
+ file: join14(agentsDir, e2.name)
3110
3324
  })) : [];
3111
3325
  return [...planned.filter((e2) => !e2.target.startsWith(".claude/agents/")), ...onDisk];
3112
3326
  }
@@ -3314,9 +3528,27 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
3314
3528
  log(assetRow("skip", `${dir} orphan prune`, `${removed.length} removed`));
3315
3529
  }
3316
3530
  }
3531
+ if (baseline.updateMode.skippedGroups.length > 0) {
3532
+ log(
3533
+ assetRow(
3534
+ "skip",
3535
+ "not selected \u2014 untouched",
3536
+ `${baseline.updateMode.skippedGroups.join(", ")} \xB7 run again with --only or the wizard to include`
3537
+ )
3538
+ );
3539
+ }
3317
3540
  if (baseline.updateMode.claudeMdUpdated) {
3318
3541
  log(assetRow("success", HARNESS_ANCHOR_FILE, "refreshed from template"));
3319
3542
  }
3543
+ if (baseline.updateMode.anchorBackedUp) {
3544
+ log(
3545
+ assetRow(
3546
+ "skip",
3547
+ `${HARNESS_ANCHOR_FILE} edited`,
3548
+ "your edits backed up as *.backup-<time> \u2014 latest template is now active"
3549
+ )
3550
+ );
3551
+ }
3320
3552
  if (baseline.updateMode.anchorCreated) {
3321
3553
  log(assetRow("success", HARNESS_ANCHOR_FILE, "created from template (anchor migration)"));
3322
3554
  }
@@ -3350,6 +3582,15 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
3350
3582
  )
3351
3583
  );
3352
3584
  }
3585
+ if (baseline.updateMode.skillsPruned.length > 0) {
3586
+ log(
3587
+ assetRow(
3588
+ "success",
3589
+ ".claude/skills not in bundle",
3590
+ `${baseline.updateMode.skillsPruned.length} files deleted \xB7 ${baseline.updateMode.skillsPruned.join(", ")}`
3591
+ )
3592
+ );
3593
+ }
3353
3594
  if (baseline.updateMode.skillsSkippedLinks.length > 0) {
3354
3595
  log(
3355
3596
  assetRow(
@@ -3844,8 +4085,8 @@ function registerInstallCommand(cli2) {
3844
4085
 
3845
4086
  // src/commands/list.ts
3846
4087
  init_esm_shims();
3847
- import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
3848
- import { join as join14, resolve as resolve3 } from "path";
4088
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
4089
+ import { join as join15, resolve as resolve3 } from "path";
3849
4090
  function listAction(options = {}, deps = {}) {
3850
4091
  const log = deps.log ?? console.log;
3851
4092
  const err = deps.err ?? console.error;
@@ -3908,8 +4149,8 @@ function formatTemplateRows(log, projectDir) {
3908
4149
  const rows = [` ${c.dim(dirs.join(" "))}`];
3909
4150
  const rootMd = log.templates.rootClaudeMd;
3910
4151
  if (rootMd) {
3911
- const path = join14(projectDir, rootMd.path);
3912
- const modified = existsSync15(path) && hashContent(readFileSync13(path, "utf8")) !== rootMd.sha256;
4152
+ const path = join15(projectDir, rootMd.path);
4153
+ const modified = existsSync16(path) && hashContent(readFileSync14(path, "utf8")) !== rootMd.sha256;
3913
4154
  rows.push(
3914
4155
  modified ? ` ${c.dim(rootMd.path)} ${c.yellow("(\uC218\uC815\uB428 \u2014 uninstall \uC2DC \uBCF4\uC874)")}` : ` ${c.dim(rootMd.path)}`
3915
4156
  );
@@ -3917,7 +4158,7 @@ function formatTemplateRows(log, projectDir) {
3917
4158
  return rows;
3918
4159
  }
3919
4160
  function formatRootFileRows(rootFiles, projectDir) {
3920
- const present = rootFiles.filter((f) => existsSync15(join14(projectDir, f.path)));
4161
+ const present = rootFiles.filter((f) => existsSync16(join15(projectDir, f.path)));
3921
4162
  const width = Math.max(0, ...present.map((f) => f.path.length));
3922
4163
  return present.map(
3923
4164
  (f) => ` ${c.dim(padDisplay(f.path, width))} ${c.dim(f.change === "created" ? "\uC0DD\uC131" : "\uBCD1\uD569")} ${c.dim(f.notes.join(" / "))}`
@@ -3935,15 +4176,15 @@ function registerListCommand(cli2) {
3935
4176
  init_esm_shims();
3936
4177
  import { spawnSync as spawnSync2 } from "child_process";
3937
4178
  import {
3938
- existsSync as existsSync16,
4179
+ existsSync as existsSync17,
3939
4180
  lstatSync as lstatSync2,
3940
4181
  readdirSync as readdirSync6,
3941
- readFileSync as readFileSync14,
3942
- rmdirSync,
4182
+ readFileSync as readFileSync15,
4183
+ rmdirSync as rmdirSync2,
3943
4184
  rmSync,
3944
4185
  writeFileSync as writeFileSync7
3945
4186
  } from "fs";
3946
- import { dirname as dirname5, join as join15, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
4187
+ import { dirname as dirname5, join as join16, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
3947
4188
 
3948
4189
  // src/uninstall-interactive.ts
3949
4190
  init_esm_shims();
@@ -5183,7 +5424,7 @@ function settleLog(ctx, io) {
5183
5424
  return true;
5184
5425
  }
5185
5426
  } else {
5186
- io.rm(join15(projectDir, INSTALL_LOG_DIR));
5427
+ io.rm(join16(projectDir, INSTALL_LOG_DIR));
5187
5428
  io.rm(legacyInstallLogPath(projectDir));
5188
5429
  io.log(` ${status.success("install log removed")}`);
5189
5430
  }
@@ -5233,7 +5474,7 @@ function advisoryLines(plan, projectDir, rootFiles) {
5233
5474
  return lines;
5234
5475
  }
5235
5476
  function rootFileAdvisoryLines(rootFiles, projectDir) {
5236
- const present = rootFiles.filter((f) => existsSync16(join15(projectDir, f.path)));
5477
+ const present = rootFiles.filter((f) => existsSync17(join16(projectDir, f.path)));
5237
5478
  if (present.length === 0) return [];
5238
5479
  return [
5239
5480
  "",
@@ -5334,16 +5575,16 @@ function buildGlobalAdvisoryCmd(asset) {
5334
5575
  }
5335
5576
  }
5336
5577
  function removeTemplates(log, projectDir, rm) {
5337
- rm(join15(projectDir, log.templates.claudeDir));
5338
- if (log.templates.codexDir) rm(join15(projectDir, log.templates.codexDir));
5339
- if (log.templates.opencodeDir) rm(join15(projectDir, log.templates.opencodeDir));
5578
+ rm(join16(projectDir, log.templates.claudeDir));
5579
+ if (log.templates.codexDir) rm(join16(projectDir, log.templates.codexDir));
5580
+ if (log.templates.opencodeDir) rm(join16(projectDir, log.templates.opencodeDir));
5340
5581
  const external = removeExternalFiles(log, projectDir, rm);
5341
5582
  const importStripped = stripRootImport(projectDir);
5342
5583
  const rootMd = log.templates.rootClaudeMd;
5343
5584
  if (rootMd) {
5344
5585
  if (rootClaudeMdModified(log, projectDir))
5345
5586
  return { rootClaudeMdKept: true, importStripped, external };
5346
- rm(join15(projectDir, rootMd.path));
5587
+ rm(join16(projectDir, rootMd.path));
5347
5588
  }
5348
5589
  return { rootClaudeMdKept: false, importStripped, external };
5349
5590
  }
@@ -5351,17 +5592,17 @@ function removeExternalFiles(log, projectDir, rm) {
5351
5592
  const removed = [];
5352
5593
  const kept = [];
5353
5594
  for (const { path, sha256 } of log.externalFiles ?? []) {
5354
- const abs = join15(projectDir, path);
5355
- if (!existsSync16(abs)) continue;
5595
+ const abs = join16(projectDir, path);
5596
+ if (!existsSync17(abs)) continue;
5356
5597
  if (!lstatSync2(abs).isFile()) continue;
5357
- if (hashContent(readFileSync14(abs, "utf8")) !== sha256) {
5598
+ if (hashContent(readFileSync15(abs, "utf8")) !== sha256) {
5358
5599
  kept.push(path);
5359
5600
  continue;
5360
5601
  }
5361
5602
  rm(abs);
5362
5603
  removed.push(path);
5363
5604
  }
5364
- for (const path of removed) pruneEmptyDirsUpward(projectDir, dirname5(join15(projectDir, path)));
5605
+ for (const path of removed) pruneEmptyDirsUpward(projectDir, dirname5(join16(projectDir, path)));
5365
5606
  return { removed, kept };
5366
5607
  }
5367
5608
  function pruneEmptyDirsUpward(projectDir, startDir) {
@@ -5371,7 +5612,7 @@ function pruneEmptyDirsUpward(projectDir, startDir) {
5371
5612
  if (rel === "" || rel.startsWith(`..${sep2}`) || rel === "..") return;
5372
5613
  try {
5373
5614
  if (readdirSync6(cursor).length > 0) return;
5374
- rmdirSync(cursor);
5615
+ rmdirSync2(cursor);
5375
5616
  } catch {
5376
5617
  return;
5377
5618
  }
@@ -5383,7 +5624,7 @@ function stripRootImport(projectDir) {
5383
5624
  const stripped = current === null ? null : stripHarnessImport(current);
5384
5625
  if (stripped === null) return false;
5385
5626
  try {
5386
- writeFileSync7(join15(projectDir, "CLAUDE.md"), stripped, "utf8");
5627
+ writeFileSync7(join16(projectDir, "CLAUDE.md"), stripped, "utf8");
5387
5628
  return true;
5388
5629
  } catch {
5389
5630
  return false;
@@ -5393,9 +5634,9 @@ function previewExternalLines(installLog, projectDir) {
5393
5634
  const lines = [];
5394
5635
  let removable = 0;
5395
5636
  for (const { path, sha256 } of installLog.externalFiles ?? []) {
5396
- const abs = join15(projectDir, path);
5397
- if (!existsSync16(abs)) continue;
5398
- if (hashContent(readFileSync14(abs, "utf8")) !== sha256) {
5637
+ const abs = join16(projectDir, path);
5638
+ if (!existsSync17(abs)) continue;
5639
+ if (hashContent(readFileSync15(abs, "utf8")) !== sha256) {
5399
5640
  lines.push(` \u25CB keep ${path} (modified since install \u2014 preserved)`);
5400
5641
  continue;
5401
5642
  }
@@ -5421,10 +5662,10 @@ function hasRootImport(projectDir) {
5421
5662
  return current !== null && stripHarnessImport(current) !== null;
5422
5663
  }
5423
5664
  function readRootClaudeMd(projectDir) {
5424
- const path = join15(projectDir, "CLAUDE.md");
5425
- if (!existsSync16(path)) return null;
5665
+ const path = join16(projectDir, "CLAUDE.md");
5666
+ if (!existsSync17(path)) return null;
5426
5667
  try {
5427
- return readFileSync14(path, "utf8");
5668
+ return readFileSync15(path, "utf8");
5428
5669
  } catch {
5429
5670
  return null;
5430
5671
  }
@@ -5432,9 +5673,9 @@ function readRootClaudeMd(projectDir) {
5432
5673
  function rootClaudeMdModified(log, projectDir) {
5433
5674
  const rootMd = log.templates.rootClaudeMd;
5434
5675
  if (!rootMd) return false;
5435
- const path = join15(projectDir, rootMd.path);
5436
- if (!existsSync16(path)) return false;
5437
- return hashContent(readFileSync14(path, "utf8")) !== rootMd.sha256;
5676
+ const path = join16(projectDir, rootMd.path);
5677
+ if (!existsSync17(path)) return false;
5678
+ return hashContent(readFileSync15(path, "utf8")) !== rootMd.sha256;
5438
5679
  }
5439
5680
  function formatTemplateList(log) {
5440
5681
  const items = [log.templates.claudeDir];
@@ -5446,7 +5687,7 @@ function defaultSpawn2(cmd, args) {
5446
5687
  return spawnSync2(cmd, [...args], { encoding: "utf8", stdio: "pipe", timeout: 12e4 });
5447
5688
  }
5448
5689
  function defaultRm(path) {
5449
- if (existsSync16(path)) {
5690
+ if (existsSync17(path)) {
5450
5691
  rmSync(path, { recursive: true, force: true });
5451
5692
  }
5452
5693
  }
@@ -5494,8 +5735,8 @@ import { resolve as resolve5 } from "path";
5494
5735
 
5495
5736
  // src/state.ts
5496
5737
  init_esm_shims();
5497
- import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
5498
- import { join as join16 } from "path";
5738
+ import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
5739
+ import { join as join17 } from "path";
5499
5740
  var META_FILE = ".claude/.installed-tracks";
5500
5741
  var LEGACY_SIGNATURES = [
5501
5742
  { rule: "htmx.md", track: "ssr-htmx" },
@@ -5505,11 +5746,11 @@ var LEGACY_SIGNATURES = [
5505
5746
  { rule: "cli-development.md", track: "tooling" }
5506
5747
  ];
5507
5748
  function detectInstallState(projectDir) {
5508
- const claudeDir = join16(projectDir, ".claude");
5509
- const hasClaudeDir = existsSync17(claudeDir);
5749
+ const claudeDir = join17(projectDir, ".claude");
5750
+ const hasClaudeDir = existsSync18(claudeDir);
5510
5751
  if (hasClaudeDir) {
5511
- const metaPath = join16(projectDir, META_FILE);
5512
- if (existsSync17(metaPath)) {
5752
+ const metaPath = join17(projectDir, META_FILE);
5753
+ if (existsSync18(metaPath)) {
5513
5754
  const tracks2 = readMetafile(metaPath);
5514
5755
  return { state: "existing", tracks: tracks2, source: "metafile", hasClaudeDir: true };
5515
5756
  }
@@ -5531,7 +5772,7 @@ function tracksFromLog(tracks) {
5531
5772
  return [...new Set(tracks.filter(isTrack))].sort();
5532
5773
  }
5533
5774
  function readMetafile(path) {
5534
- const raw = readFileSync15(path, "utf8");
5775
+ const raw = readFileSync16(path, "utf8");
5535
5776
  const seen = /* @__PURE__ */ new Set();
5536
5777
  for (const line of raw.split(/\s+/)) {
5537
5778
  const trimmed = line.trim();
@@ -5542,13 +5783,13 @@ function readMetafile(path) {
5542
5783
  return [...seen].sort();
5543
5784
  }
5544
5785
  function inferFromLegacySignatures(projectDir) {
5545
- const rulesDir = join16(projectDir, ".claude/rules");
5546
- if (!existsSync17(rulesDir)) {
5786
+ const rulesDir = join17(projectDir, ".claude/rules");
5787
+ if (!existsSync18(rulesDir)) {
5547
5788
  return [];
5548
5789
  }
5549
5790
  const found = /* @__PURE__ */ new Set();
5550
5791
  for (const sig of LEGACY_SIGNATURES) {
5551
- if (existsSync17(join16(rulesDir, sig.rule))) {
5792
+ if (existsSync18(join17(rulesDir, sig.rule))) {
5552
5793
  found.add(sig.track);
5553
5794
  }
5554
5795
  }
@@ -5570,8 +5811,21 @@ function updateAction(options = {}, deps = {}) {
5570
5811
  exit(1);
5571
5812
  return;
5572
5813
  }
5814
+ const rawOnly = options.only === void 0 ? [] : [options.only].flat().map((v2) => typeof v2 === "string" ? v2 : "(missing value)");
5815
+ const parsed = parseUpdateOnly(rawOnly);
5816
+ if (!parsed.ok) {
5817
+ err(status.failure(c.red(`Unknown --only value: ${parsed.invalid.join(", ")}`)));
5818
+ err(c.dim(` Valid groups: ${UPDATE_GROUPS.join(" | ")}`));
5819
+ exit(1);
5820
+ return;
5821
+ }
5573
5822
  log(c.dim(`Updating installed harness files in ${projectDir}`));
5574
- execute(buildUpdateSpec(projectDir, state.tracks), { log, err, exit, mode: "update" });
5823
+ execute(buildUpdateSpec(projectDir, state.tracks, parsed.groups), {
5824
+ log,
5825
+ err,
5826
+ exit,
5827
+ mode: "update"
5828
+ });
5575
5829
  }
5576
5830
  function registerUpdateCommand(cli2) {
5577
5831
  cli2.command(
@@ -5579,7 +5833,10 @@ function registerUpdateCommand(cli2) {
5579
5833
  "Refresh installed policy files (rules / agents / commands / hooks / skills)"
5580
5834
  ).option("--project-dir <path>", "[Project] Target project directory", {
5581
5835
  default: process.cwd()
5582
- }).action((options) => {
5836
+ }).option(
5837
+ "--only <group>",
5838
+ `[Scope] Update only this group (repeatable): ${UPDATE_GROUPS.join(" | ")}`
5839
+ ).action((options) => {
5583
5840
  updateAction(options);
5584
5841
  });
5585
5842
  }
@@ -5694,19 +5951,6 @@ var INSTALL_TARGET_PAGES = [
5694
5951
  { label: "Visual & Media (Slides \xB7 Diagrams \xB7 Motion \xB7 Video)", cats: ["visual-media"] },
5695
5952
  { label: "Workflow & ECC Suite", cats: ["workflow", "ecc-suite"] }
5696
5953
  ];
5697
- var DEV_METHOD_BUNDLE_VALUE = "bundle:dev-method";
5698
- var DEV_METHOD_BUNDLE_CATEGORY = "workflow";
5699
- var bundleMemberValues = () => DEV_METHOD_SKILL_IDS.map((id) => `asset:${id}`);
5700
- function collapseDevMethodBundle(ids) {
5701
- const members = new Set(bundleMemberValues());
5702
- const rest = ids.filter((v2) => !members.has(v2));
5703
- const anyMember = ids.some((v2) => members.has(v2));
5704
- return anyMember ? [...rest, DEV_METHOD_BUNDLE_VALUE] : rest;
5705
- }
5706
- function expandDevMethodBundle(ids) {
5707
- const rest = ids.filter((v2) => v2 !== DEV_METHOD_BUNDLE_VALUE);
5708
- return ids.includes(DEV_METHOD_BUNDLE_VALUE) ? [...rest, ...bundleMemberValues()] : rest;
5709
- }
5710
5954
  var BASELINE_TITLES = {
5711
5955
  rules: "Rules \u2014 \uC0C1\uC2DC \uC0C1\uC8FC\uD558\uB294 \uB300\uC6D0\uCE59 (4 CLI \uC804\uBD80\uC5D0 \uC124\uCE58)",
5712
5956
  hooks: "Hooks \u2014 \uACB0\uC815\uB860\uC801 \uAC00\uB4DC (\uD574\uC81C\uD558\uBA74 settings.json \uBC30\uC120\uB3C4 \uD568\uAED8 \uBE60\uC9C4\uB2E4)",
@@ -5720,7 +5964,8 @@ function buildPageGroups(cats, initialSet, installedSet = /* @__PURE__ */ new Se
5720
5964
  for (const kind of baselineKinds) {
5721
5965
  const items = baselineTargets.filter((t) => t.kind === kind).map((t) => ({
5722
5966
  value: t.id,
5723
- label: ` ${t.name}${installedMark(t.id)}`
5967
+ label: ` ${t.name}${installedMark(t.id)}`,
5968
+ ...t.hint ? { hint: t.hint } : {}
5724
5969
  }));
5725
5970
  if (items.length === 0) continue;
5726
5971
  groups[BASELINE_TITLES[kind]] = items;
@@ -5736,16 +5981,10 @@ function buildPageGroups(cats, initialSet, installedSet = /* @__PURE__ */ new Se
5736
5981
  hint: o.hint
5737
5982
  });
5738
5983
  }
5739
- if (cat === DEV_METHOD_BUNDLE_CATEGORY) {
5740
- items.push({
5741
- value: DEV_METHOD_BUNDLE_VALUE,
5742
- label: ` uzys \uD558\uB124\uC2A4 \uBC29\uBC95\uB860 ${DEV_METHOD_SKILL_IDS.length}\uC885 [uzys] \u2605 official${installedMark(DEV_METHOD_BUNDLE_VALUE)}`,
5743
- hint: DEV_METHOD_SKILL_IDS.join(", ")
5744
- });
5745
- }
5746
5984
  const tierOrder = { official: 0, vetted: 1, experimental: 2 };
5747
- const devMethod = new Set(DEV_METHOD_SKILL_IDS);
5748
- const catAssets = [...EXTERNAL_ASSETS.filter((x) => x.category === cat)].filter((x) => !devMethod.has(x.id)).sort((a, b2) => tierOrder[assetTrustTier(a.id)] - tierOrder[assetTrustTier(b2.id)]);
5985
+ const catAssets = [...EXTERNAL_ASSETS.filter((x) => x.category === cat)].sort(
5986
+ (a, b2) => tierOrder[assetTrustTier(a.id)] - tierOrder[assetTrustTier(b2.id)]
5987
+ );
5749
5988
  for (const a of catAssets) {
5750
5989
  const tier = assetTrustTier(a.id);
5751
5990
  const badge = tier === "official" ? " \u2605 official" : tier === "experimental" ? " \u26A0 experimental (opt-in)" : "";
@@ -5851,6 +6090,42 @@ var defaultPrompts = {
5851
6090
  });
5852
6091
  return q(result) ? null : result;
5853
6092
  },
6093
+ selectUpdateGroups: async () => {
6094
+ const result = await ve({
6095
+ message: "What to update (space = toggle \xB7 your edits are always backed up)",
6096
+ options: [
6097
+ { value: "skills", label: "Skills", hint: "installed bundled skills \u2192 latest" },
6098
+ {
6099
+ value: "new-skills",
6100
+ label: "New skills",
6101
+ hint: "bundled skills added since your install"
6102
+ },
6103
+ {
6104
+ value: "rules",
6105
+ label: "Rules \xB7 agents \xB7 commands",
6106
+ hint: "your edited files \u2192 *.backup-<time>"
6107
+ },
6108
+ {
6109
+ value: "anchor",
6110
+ label: "CLAUDE-uzys-harness.md anchor",
6111
+ hint: "your edited anchor \u2192 *.backup-<time>"
6112
+ },
6113
+ {
6114
+ value: "hooks",
6115
+ label: "Hooks \xB7 settings.json",
6116
+ hint: "stale hook refs cleaned"
6117
+ },
6118
+ {
6119
+ value: "external",
6120
+ label: "Codex / OpenCode / Antigravity + external skills",
6121
+ hint: "rendered artifacts + npx skills"
6122
+ }
6123
+ ],
6124
+ initialValues: [...UPDATE_GROUPS],
6125
+ required: true
6126
+ });
6127
+ return q(result) ? null : result;
6128
+ },
5854
6129
  confirmInstall: async (summary) => {
5855
6130
  const result = await ue({
5856
6131
  message: `${summary}
@@ -5862,14 +6137,11 @@ Proceed?`,
5862
6137
  },
5863
6138
  selectInstallTargets: async (initialChecked, step, recap) => {
5864
6139
  const pages = INSTALL_TARGET_PAGES;
5865
- const baselineTargets = listBaselineTargets({ tracks: recap?.tracks ?? [] });
5866
- const displayInitial = collapseDevMethodBundle(initialChecked);
5867
- const initialSet = new Set(displayInitial);
5868
- const collected = new Set(displayInitial);
6140
+ const baselineTargets = withBaselineHints(listBaselineTargets({ tracks: recap?.tracks ?? [] }));
6141
+ const initialSet = new Set(initialChecked);
6142
+ const collected = new Set(initialChecked);
5869
6143
  const recapLine = recap ? `Tracks: ${recap.tracks.join(", ")} \xB7 CLIs: ${recap.cli.join(", ")}` : "";
5870
- const installedSet = new Set(
5871
- collapseDevMethodBundle(recap?.installed ?? [])
5872
- );
6144
+ const installedSet = new Set(recap?.installed ?? []);
5873
6145
  process.stdout.write("\x1B[?1049h");
5874
6146
  let resultIds = null;
5875
6147
  let aborted = false;
@@ -5929,7 +6201,7 @@ Proceed?`,
5929
6201
  pageIdx++;
5930
6202
  }
5931
6203
  if (!aborted) {
5932
- resultIds = expandDevMethodBundle([...collected]);
6204
+ resultIds = [...collected];
5933
6205
  }
5934
6206
  } finally {
5935
6207
  process.stdout.write("\x1B[?1049l");
@@ -6019,11 +6291,15 @@ async function runInteractive(projectDir, deps = {}) {
6019
6291
  }
6020
6292
  if (action === "update") {
6021
6293
  mode = "update";
6022
- const spec = buildUpdateSpec(projectDir, state.tracks);
6023
- const confirmed = await prompts.confirmInstall(
6024
- `UPDATE policy files only:
6025
- ${formatSummary(spec)}`
6026
- );
6294
+ const groups = prompts.selectUpdateGroups ? await prompts.selectUpdateGroups() : null;
6295
+ if (groups === null && prompts.selectUpdateGroups) {
6296
+ prompts.cancel("Cancelled.");
6297
+ return { ok: false, reason: "cancelled" };
6298
+ }
6299
+ const spec = buildUpdateSpec(projectDir, state.tracks, groups ?? void 0);
6300
+ const scopeLine = spec.updateOnly ? `UPDATE ${spec.updateOnly.join(" \xB7 ")} only (untouched: ${UPDATE_GROUPS.filter((g) => !spec.updateOnly?.includes(g)).join(", ")}):` : "UPDATE installed harness files:";
6301
+ const confirmed = await prompts.confirmInstall(`${scopeLine}
6302
+ ${formatSummary(spec)}`);
6027
6303
  if (!confirmed) {
6028
6304
  prompts.outro("Cancelled.");
6029
6305
  return { ok: false, reason: "cancelled" };
@@ -6170,7 +6446,7 @@ function formatSummary(spec) {
6170
6446
  const cost = formatResidentCostLine(
6171
6447
  residentCost(
6172
6448
  buildManifest(assetSpec).filter(
6173
- (e2) => e2.applies(assetSpec) && !isBaselineExcluded(e2.target, baselineExcluded)
6449
+ (e2) => e2.applies(assetSpec) && !isBaselineExcluded(e2.target, baselineExcluded) && landsOnDisk(e2.target, spec.cli)
6174
6450
  )
6175
6451
  ),
6176
6452
  summarizeContextCost(finalAssets).unmeasuredCount