@uzysjung/agent-harness 26.152.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.
Files changed (28) hide show
  1. package/dist/{chunk-EQDC2AAU.js → chunk-242TVGMZ.js} +582 -556
  2. package/dist/chunk-242TVGMZ.js.map +1 -0
  3. package/dist/index.js +623 -303
  4. package/dist/index.js.map +1 -1
  5. package/dist/trust-tier-drift.js +1 -1
  6. package/package.json +1 -1
  7. package/templates/CLAUDE.md +93 -145
  8. package/templates/codex/README.md +6 -7
  9. package/templates/codex/config.toml.template +1 -9
  10. package/templates/skills/audit-harness-fit/README.md +76 -43
  11. package/templates/skills/audit-harness-fit/SKILL.md +62 -47
  12. package/templates/skills/audit-harness-fit/evals/scenarios.yaml +179 -22
  13. package/templates/skills/audit-harness-fit/references/apply.md +74 -45
  14. package/templates/skills/audit-harness-fit/references/audit.md +142 -118
  15. package/templates/skills/audit-harness-fit/references/populate.md +53 -49
  16. package/templates/skills/audit-harness-fit/references/verification.md +131 -100
  17. package/templates/skills/clear-korean-communication/SKILL.md +76 -209
  18. package/templates/skills/external-model-consult/scripts/codex-ask.sh +9 -4
  19. package/templates/skills/external-model-consult/scripts/gemini-ask.sh +9 -4
  20. package/templates/skills/model-orchestration/SKILL.md +123 -267
  21. package/templates/skills/objective-brief/SKILL.md +3 -3
  22. package/dist/chunk-EQDC2AAU.js.map +0 -1
  23. package/templates/codex/hooks/README.md +0 -37
  24. package/templates/codex/hooks/session-start.sh +0 -7
  25. package/templates/codex/hooks/uncommitted-check.sh +0 -7
  26. package/templates/skills/clear-korean-communication/references/pre-send-checklist.md +0 -37
  27. package/templates/skills/clear-korean-communication/references/why-it-works.md +0 -24
  28. package/templates/skills/clear-korean-communication/references/worked-examples.md +0 -89
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,
@@ -18,6 +17,8 @@ import {
18
17
  RETIRED_AGENT_IDS,
19
18
  RETIRED_SKILL_IDS,
20
19
  TRACKS,
20
+ TRACK_AGENTS,
21
+ UPDATE_GROUPS,
21
22
  __commonJS,
22
23
  __toESM,
23
24
  anyTrack,
@@ -38,6 +39,7 @@ import {
38
39
  ensureDir,
39
40
  ensureProjectSkeleton,
40
41
  experimentalOptInCandidates,
42
+ extractFrontmatter,
41
43
  filterApplicableAssets,
42
44
  formatResidentCostLine,
43
45
  hasUiTrack,
@@ -50,12 +52,14 @@ import {
50
52
  isHarnessOwned,
51
53
  isInstallScope,
52
54
  isTrack,
55
+ landsOnDisk,
53
56
  legacyInstallLogPath,
54
57
  listFilesRecursive,
55
58
  mergeExternalFiles,
56
59
  readInstallLog,
57
60
  renderFillScaffold,
58
61
  residentCost,
62
+ resolveBundleRoot,
59
63
  resolveRules,
60
64
  resolveScope,
61
65
  stripHarnessImport,
@@ -63,7 +67,7 @@ import {
63
67
  upsertHarnessImport,
64
68
  withContinuousSkillsNote,
65
69
  writeInstallLog
66
- } from "./chunk-EQDC2AAU.js";
70
+ } from "./chunk-242TVGMZ.js";
67
71
 
68
72
  // node_modules/sisteransi/src/index.js
69
73
  var require_src = __commonJS({
@@ -748,7 +752,7 @@ var cac = (name = "") => new CAC(name);
748
752
  // package.json
749
753
  var package_default = {
750
754
  name: "@uzysjung/agent-harness",
751
- version: "26.152.0",
755
+ version: "26.154.0",
752
756
  description: "Curate vetted AI-coding skills & plugins by your tech stack \u2014 install only what you need, across Claude Code, Codex, OpenCode & Antigravity",
753
757
  type: "module",
754
758
  publishConfig: {
@@ -813,6 +817,8 @@ import { fileURLToPath } from "url";
813
817
 
814
818
  // src/baseline-targets.ts
815
819
  init_esm_shims();
820
+ import { existsSync, readFileSync } from "fs";
821
+ import { join } from "path";
816
822
  var BASELINE_KINDS = ["rules", "agents", "hooks", "skills"];
817
823
  var BASELINE_PREFIX = "baseline:";
818
824
  var PREFIX_BY_KIND = [
@@ -850,6 +856,42 @@ function isBaselineExcluded(target, excluded) {
850
856
  const t = classifyBaselineTarget(target);
851
857
  return t !== null && excluded.has(t.id);
852
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
+ }
853
895
 
854
896
  // src/cli-targets.ts
855
897
  init_esm_shims();
@@ -987,18 +1029,18 @@ function padDisplay(s, width) {
987
1029
  init_esm_shims();
988
1030
  import {
989
1031
  chmodSync as chmodSync3,
990
- existsSync as existsSync13,
1032
+ existsSync as existsSync14,
991
1033
  mkdirSync as mkdirSync4,
992
1034
  readdirSync as readdirSync4,
993
- readFileSync as readFileSync12,
1035
+ readFileSync as readFileSync13,
994
1036
  writeFileSync as writeFileSync6
995
1037
  } from "fs";
996
- 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";
997
1039
 
998
1040
  // src/ci-scaffold.ts
999
1041
  init_esm_shims();
1000
- import { existsSync } from "fs";
1001
- import { join } from "path";
1042
+ import { existsSync as existsSync2 } from "fs";
1043
+ import { join as join2 } from "path";
1002
1044
  var PYTHON_TRACKS = "data|csr-fastapi|full";
1003
1045
  var VARIANTS = [
1004
1046
  {
@@ -1022,12 +1064,12 @@ function installCiScaffold(ctx) {
1022
1064
  for (const v2 of VARIANTS) {
1023
1065
  if (!v2.applies(ctx.tracks)) continue;
1024
1066
  const relTarget = `.github/workflows/${v2.target}`;
1025
- const target = join(ctx.projectDir, relTarget);
1026
- if (existsSync(target)) {
1067
+ const target = join2(ctx.projectDir, relTarget);
1068
+ if (existsSync2(target)) {
1027
1069
  report.skippedExisting.push(relTarget);
1028
1070
  continue;
1029
1071
  }
1030
- copyFile(join(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
1072
+ copyFile(join2(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
1031
1073
  report.written.push(relTarget);
1032
1074
  }
1033
1075
  return report;
@@ -1038,8 +1080,8 @@ init_esm_shims();
1038
1080
 
1039
1081
  // src/antigravity/transform.ts
1040
1082
  init_esm_shims();
1041
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1042
- 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";
1043
1085
 
1044
1086
  // src/codex/agents-md.ts
1045
1087
  init_esm_shims();
@@ -1054,8 +1096,8 @@ function renderAgentsMd(params) {
1054
1096
 
1055
1097
  // src/codex/skills.ts
1056
1098
  init_esm_shims();
1057
- import { chmodSync, existsSync as existsSync2, readFileSync } from "fs";
1058
- import { join as join2 } from "path";
1099
+ import { chmodSync, existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1100
+ import { join as join3 } from "path";
1059
1101
  function renderBundledSkill(source) {
1060
1102
  const trimmed = source.trimEnd();
1061
1103
  const lines = trimmed.split(/\r?\n/);
@@ -1087,14 +1129,14 @@ function writeBundledSkillDirs(params) {
1087
1129
  const { harnessRoot, projectDir, skillIds, writer } = params;
1088
1130
  const written = [];
1089
1131
  for (const id of skillIds) {
1090
- const srcDir = join2(harnessRoot, "templates/skills", id);
1091
- const srcSkill = join2(srcDir, "SKILL.md");
1092
- if (!existsSync2(srcSkill)) {
1132
+ const srcDir = join3(harnessRoot, "templates/skills", id);
1133
+ const srcSkill = join3(srcDir, "SKILL.md");
1134
+ if (!existsSync3(srcSkill)) {
1093
1135
  continue;
1094
1136
  }
1095
- const targetDir = join2(projectDir, ".agents", "skills", id);
1096
- const targetSkill = join2(targetDir, "SKILL.md");
1097
- 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")))) {
1098
1140
  continue;
1099
1141
  }
1100
1142
  written.push(targetSkill);
@@ -1102,10 +1144,10 @@ function writeBundledSkillDirs(params) {
1102
1144
  if (rel === "SKILL.md") continue;
1103
1145
  if (rel.split("/").some((segment) => segment.startsWith("."))) continue;
1104
1146
  const segments = rel.split("/");
1105
- const raw = readFileSync(join2(srcDir, ...segments), "utf8");
1147
+ const raw = readFileSync2(join3(srcDir, ...segments), "utf8");
1106
1148
  const ported = rel.endsWith(".md") || rel.endsWith(".sh") ? `${portBody(raw)}
1107
1149
  ` : raw;
1108
- const target = join2(targetDir, ...segments);
1150
+ const target = join3(targetDir, ...segments);
1109
1151
  if (!writer.write(target, ported, { createInRefresh: true })) continue;
1110
1152
  if (rel.endsWith(".sh")) chmodSync(target, 493);
1111
1153
  written.push(target);
@@ -1116,13 +1158,13 @@ function writeBundledSkillDirs(params) {
1116
1158
 
1117
1159
  // src/owned-write.ts
1118
1160
  init_esm_shims();
1119
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
1161
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
1120
1162
  import { dirname, relative, sep } from "path";
1121
1163
 
1122
1164
  // src/foreign-slot.ts
1123
1165
  init_esm_shims();
1124
1166
  import { lstatSync } from "fs";
1125
- import { join as join3 } from "path";
1167
+ import { join as join4 } from "path";
1126
1168
  var SKILL_SLOT = /^((?:\.claude|\.agents)\/skills\/[^/]+)(?:\/|$)/;
1127
1169
  function occupiedByNonDirectory(path) {
1128
1170
  const stat = lstatSync(path, { throwIfNoEntry: false });
@@ -1131,15 +1173,15 @@ function occupiedByNonDirectory(path) {
1131
1173
  function foreignOwnedTarget(projectDir, rel) {
1132
1174
  const slot = SKILL_SLOT.exec(rel)?.[1];
1133
1175
  if (slot === void 0) return null;
1134
- if (occupiedByNonDirectory(join3(projectDir, slot))) return slot;
1176
+ if (occupiedByNonDirectory(join4(projectDir, slot))) return slot;
1135
1177
  if (rel !== slot) {
1136
1178
  const parts = rel.slice(slot.length + 1).split("/");
1137
1179
  let cursor = slot;
1138
1180
  for (const part of parts.slice(0, -1)) {
1139
1181
  cursor = `${cursor}/${part}`;
1140
- if (occupiedByNonDirectory(join3(projectDir, cursor))) return cursor;
1182
+ if (occupiedByNonDirectory(join4(projectDir, cursor))) return cursor;
1141
1183
  }
1142
- const leaf = lstatSync(join3(projectDir, rel), { throwIfNoEntry: false });
1184
+ const leaf = lstatSync(join4(projectDir, rel), { throwIfNoEntry: false });
1143
1185
  if (leaf !== void 0 && !leaf.isFile()) return rel;
1144
1186
  }
1145
1187
  return null;
@@ -1162,13 +1204,13 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
1162
1204
  return false;
1163
1205
  }
1164
1206
  const digest = hashContent(content);
1165
- if (!existsSync3(absPath)) {
1207
+ if (!existsSync4(absPath)) {
1166
1208
  if (refreshOnly && !opts?.createInRefresh) return false;
1167
1209
  mkdirSync(dirname(absPath), { recursive: true });
1168
1210
  writeFileSync(absPath, content);
1169
1211
  updated++;
1170
1212
  } else {
1171
- const current = readFileSync2(absPath, "utf8");
1213
+ const current = readFileSync3(absPath, "utf8");
1172
1214
  if (current !== content) {
1173
1215
  if (!isHarnessOwned(baseline, rel, current)) {
1174
1216
  backupPaths.push(backupFile(absPath, now));
@@ -1195,8 +1237,8 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
1195
1237
 
1196
1238
  // src/rules-port.ts
1197
1239
  init_esm_shims();
1198
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1199
- import { join as join4 } from "path";
1240
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1241
+ import { join as join5 } from "path";
1200
1242
  function stripClaudeFrontmatter(source) {
1201
1243
  if (!source.startsWith("---\n")) return source.trim();
1202
1244
  const end = source.indexOf("\n---", 4);
@@ -1206,9 +1248,9 @@ function stripClaudeFrontmatter(source) {
1206
1248
  function portRules(harnessRoot, ruleNames) {
1207
1249
  const ported = [];
1208
1250
  for (const name of ruleNames) {
1209
- const src = join4(harnessRoot, "templates/rules", `${name}.md`);
1210
- if (!existsSync4(src)) continue;
1211
- 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")) });
1212
1254
  }
1213
1255
  return ported;
1214
1256
  }
@@ -1232,7 +1274,7 @@ function runAntigravityTransform(params) {
1232
1274
  const antigravityInstalled = rulesFile !== null;
1233
1275
  const harnessRuleFiles = [];
1234
1276
  for (const rule of portRules(harnessRoot, rules)) {
1235
- const target = join5(projectDir, ".agents", "rules", `${rule.name}.md`);
1277
+ const target = join6(projectDir, ".agents", "rules", `${rule.name}.md`);
1236
1278
  if (!writer.write(target, `${rule.body}
1237
1279
  `, { createInRefresh: antigravityInstalled })) {
1238
1280
  continue;
@@ -1253,14 +1295,14 @@ function runAntigravityTransform(params) {
1253
1295
  };
1254
1296
  }
1255
1297
  function writeRules(harnessRoot, projectDir, writer, selectedInternalSkills) {
1256
- const claudeMdPath = join5(harnessRoot, "templates/CLAUDE.md");
1257
- const templatePath = join5(harnessRoot, "templates/antigravity/AGENTS.md.template");
1258
- 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)) {
1259
1301
  return null;
1260
1302
  }
1261
- const claudeMd = readFileSync4(claudeMdPath, "utf8");
1262
- const template = readFileSync4(templatePath, "utf8");
1263
- 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");
1264
1306
  const rulesOut = renderAgentsMd({
1265
1307
  template,
1266
1308
  claudeMd,
@@ -1277,18 +1319,18 @@ function writeRules(harnessRoot, projectDir, writer, selectedInternalSkills) {
1277
1319
  // src/codex/opt-in.ts
1278
1320
  init_esm_shims();
1279
1321
  import { homedir } from "os";
1280
- import { join as join6 } from "path";
1322
+ import { join as join7 } from "path";
1281
1323
 
1282
1324
  // src/codex/trust-entry.ts
1283
1325
  init_esm_shims();
1284
- 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";
1285
1327
  import { dirname as dirname2 } from "path";
1286
1328
  var TRUST_BLOCK_REGEX = /\[projects\."([^"]+)"\]/g;
1287
1329
  function registerTrustEntry(opts) {
1288
1330
  const { configPath, projectDir } = opts;
1289
1331
  try {
1290
1332
  mkdirSync2(dirname2(configPath), { recursive: true });
1291
- const existing = existsSync6(configPath) ? readFileSync5(configPath, "utf8") : "";
1333
+ const existing = existsSync7(configPath) ? readFileSync6(configPath, "utf8") : "";
1292
1334
  if (hasTrustEntry(existing, projectDir)) {
1293
1335
  return { status: "already-present" };
1294
1336
  }
@@ -1312,8 +1354,8 @@ function hasTrustEntry(configContent, projectDir) {
1312
1354
 
1313
1355
  // src/codex/opt-in.ts
1314
1356
  function runCodexOptIn(ctx) {
1315
- const codexHome = ctx.codexHome ?? join6(homedir(), ".codex");
1316
- const configPath = join6(codexHome, "config.toml");
1357
+ const codexHome = ctx.codexHome ?? join7(homedir(), ".codex");
1358
+ const configPath = join7(codexHome, "config.toml");
1317
1359
  const result = registerTrustEntry({ configPath, projectDir: ctx.projectDir });
1318
1360
  return {
1319
1361
  trustEntry: {
@@ -1326,8 +1368,8 @@ function runCodexOptIn(ctx) {
1326
1368
 
1327
1369
  // src/codex/transform.ts
1328
1370
  init_esm_shims();
1329
- import { chmodSync as chmodSync2, existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1330
- 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";
1331
1373
 
1332
1374
  // src/codex/config-toml.ts
1333
1375
  init_esm_shims();
@@ -1404,12 +1446,12 @@ function runCodexTransform(params) {
1404
1446
  refreshOnly
1405
1447
  } = params;
1406
1448
  const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
1407
- const claudeMd = readRequired(join7(harnessRoot, "templates/CLAUDE.md"));
1408
- const agentsTemplate = readRequired(join7(harnessRoot, "templates/codex/AGENTS.md.template"));
1409
- 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"));
1410
1452
  const projectName = basename2(projectDir);
1411
- const mcp = readOptionalJson(join7(harnessRoot, ".mcp.json"));
1412
- const agentsMdPath = join7(projectDir, "AGENTS.md");
1453
+ const mcp = readOptionalJson(join8(harnessRoot, ".mcp.json"));
1454
+ const agentsMdPath = join8(projectDir, "AGENTS.md");
1413
1455
  ensureDir(projectDir);
1414
1456
  const agentsMdOut = renderAgentsMd({
1415
1457
  template: agentsTemplate,
@@ -1424,7 +1466,7 @@ function runCodexTransform(params) {
1424
1466
  harnessRules: renderRulesBlock(portRules(harnessRoot, rules))
1425
1467
  });
1426
1468
  writer.write(agentsMdPath, agentsMdOut);
1427
- const configTomlPath = join7(projectDir, ".codex/config.toml");
1469
+ const configTomlPath = join8(projectDir, ".codex/config.toml");
1428
1470
  writer.write(
1429
1471
  configTomlPath,
1430
1472
  renderConfigToml({
@@ -1434,15 +1476,15 @@ function runCodexTransform(params) {
1434
1476
  mcp
1435
1477
  })
1436
1478
  );
1437
- const hookDir = join7(projectDir, ".codex/hooks");
1479
+ const hookDir = join8(projectDir, ".codex/hooks");
1438
1480
  const hookFiles = [];
1439
1481
  for (const hook of HOOK_NAMES) {
1440
- const src = join7(harnessRoot, "templates/hooks", `${hook}.sh`);
1441
- if (!existsSync7(src)) {
1482
+ const src = join8(harnessRoot, "templates/hooks", `${hook}.sh`);
1483
+ if (!existsSync8(src)) {
1442
1484
  continue;
1443
1485
  }
1444
- const ported = readFileSync6(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
1445
- 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`);
1446
1488
  if (!writer.write(target, ported)) continue;
1447
1489
  chmodSync2(target, 493);
1448
1490
  hookFiles.push(target);
@@ -1462,17 +1504,17 @@ function runCodexTransform(params) {
1462
1504
  };
1463
1505
  }
1464
1506
  function readRequired(path) {
1465
- if (!existsSync7(path)) {
1507
+ if (!existsSync8(path)) {
1466
1508
  throw new Error(`Codex transform: required source missing: ${path}`);
1467
1509
  }
1468
- return readFileSync6(path, "utf8");
1510
+ return readFileSync7(path, "utf8");
1469
1511
  }
1470
1512
  function readOptionalJson(path) {
1471
- if (!existsSync7(path)) {
1513
+ if (!existsSync8(path)) {
1472
1514
  return null;
1473
1515
  }
1474
1516
  try {
1475
- return JSON.parse(readFileSync6(path, "utf8"));
1517
+ return JSON.parse(readFileSync7(path, "utf8"));
1476
1518
  } catch {
1477
1519
  return null;
1478
1520
  }
@@ -1480,8 +1522,8 @@ function readOptionalJson(path) {
1480
1522
 
1481
1523
  // src/opencode/transform.ts
1482
1524
  init_esm_shims();
1483
- import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync7, unlinkSync } from "fs";
1484
- 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";
1485
1527
 
1486
1528
  // src/opencode/agents-md.ts
1487
1529
  init_esm_shims();
@@ -1524,15 +1566,15 @@ function runOpencodeTransform(params) {
1524
1566
  refreshOnly
1525
1567
  } = params;
1526
1568
  const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
1527
- const claudeMd = readRequired2(join8(harnessRoot, "templates/CLAUDE.md"));
1528
- 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"));
1529
1571
  const opencodeTemplate = readRequired2(
1530
- join8(harnessRoot, "templates/opencode/opencode.json.template")
1572
+ join9(harnessRoot, "templates/opencode/opencode.json.template")
1531
1573
  );
1532
1574
  const projectName = basename3(projectDir);
1533
- const mcp = readOptionalJson2(join8(harnessRoot, ".mcp.json"));
1575
+ const mcp = readOptionalJson2(join9(harnessRoot, ".mcp.json"));
1534
1576
  ensureDir(projectDir);
1535
- const agentsMdPath = join8(projectDir, "AGENTS.md");
1577
+ const agentsMdPath = join9(projectDir, "AGENTS.md");
1536
1578
  const agentsMdOut = renderAgentsMd2({
1537
1579
  template: agentsTemplate,
1538
1580
  claudeMd,
@@ -1548,7 +1590,7 @@ function runOpencodeTransform(params) {
1548
1590
  harnessRules: renderRulesBlock(portRules(harnessRoot, rules))
1549
1591
  });
1550
1592
  writer.write(agentsMdPath, agentsMdOut);
1551
- const opencodeJsonPath = join8(projectDir, "opencode.json");
1593
+ const opencodeJsonPath = join9(projectDir, "opencode.json");
1552
1594
  writer.write(opencodeJsonPath, renderOpencodeJson({ template: opencodeTemplate, mcp }));
1553
1595
  const skillFiles = writeBundledSkillDirs({
1554
1596
  harnessRoot,
@@ -1557,13 +1599,13 @@ function runOpencodeTransform(params) {
1557
1599
  writer
1558
1600
  });
1559
1601
  const retiredCommands = [];
1560
- const cmdDir = join8(projectDir, ".opencode/commands");
1561
- if (existsSync8(cmdDir)) {
1602
+ const cmdDir = join9(projectDir, ".opencode/commands");
1603
+ if (existsSync9(cmdDir)) {
1562
1604
  for (const entry of readdirSync(cmdDir)) {
1563
1605
  if (!entry.endsWith(".md")) continue;
1564
1606
  const id = entry.slice(0, -3);
1565
- if (!existsSync8(join8(harnessRoot, "templates/skills", id, "SKILL.md"))) continue;
1566
- const victim = join8(cmdDir, entry);
1607
+ if (!existsSync9(join9(harnessRoot, "templates/skills", id, "SKILL.md"))) continue;
1608
+ const victim = join9(cmdDir, entry);
1567
1609
  try {
1568
1610
  backupFile(victim);
1569
1611
  unlinkSync(victim);
@@ -1581,17 +1623,17 @@ function runOpencodeTransform(params) {
1581
1623
  };
1582
1624
  }
1583
1625
  function readRequired2(path) {
1584
- if (!existsSync8(path)) {
1626
+ if (!existsSync9(path)) {
1585
1627
  throw new Error(`OpenCode transform: required source missing: ${path}`);
1586
1628
  }
1587
- return readFileSync7(path, "utf8");
1629
+ return readFileSync8(path, "utf8");
1588
1630
  }
1589
1631
  function readOptionalJson2(path) {
1590
- if (!existsSync8(path)) {
1632
+ if (!existsSync9(path)) {
1591
1633
  return null;
1592
1634
  }
1593
1635
  try {
1594
- return JSON.parse(readFileSync7(path, "utf8"));
1636
+ return JSON.parse(readFileSync8(path, "utf8"));
1595
1637
  } catch {
1596
1638
  return null;
1597
1639
  }
@@ -1683,8 +1725,8 @@ function runCliTransforms(params) {
1683
1725
 
1684
1726
  // src/env-files.ts
1685
1727
  init_esm_shims();
1686
- import { appendFileSync, existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
1687
- 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";
1688
1730
  var ENV_EXAMPLE_BODY = `# .env.example \u2014 csr-supabase Track
1689
1731
  # Copy to .env (gitignored) and fill in values: cp .env.example .env
1690
1732
 
@@ -1721,19 +1763,19 @@ function writeEnvExample(projectDir, tracks) {
1721
1763
  if (!tracks.some((t) => ENV_EXAMPLE_TRACKS.includes(t))) {
1722
1764
  return false;
1723
1765
  }
1724
- const path = join9(projectDir, ".env.example");
1725
- if (existsSync9(path)) {
1766
+ const path = join10(projectDir, ".env.example");
1767
+ if (existsSync10(path)) {
1726
1768
  return false;
1727
1769
  }
1728
1770
  writeFileSync3(path, ENV_EXAMPLE_BODY);
1729
1771
  return true;
1730
1772
  }
1731
1773
  function addGitignoreEnv(projectDir) {
1732
- const path = join9(projectDir, ".gitignore");
1733
- if (!existsSync9(path)) {
1774
+ const path = join10(projectDir, ".gitignore");
1775
+ if (!existsSync10(path)) {
1734
1776
  return false;
1735
1777
  }
1736
- const content = readFileSync8(path, "utf8");
1778
+ const content = readFileSync9(path, "utf8");
1737
1779
  if (GITIGNORE_ENV_PATTERN.test(content)) {
1738
1780
  return false;
1739
1781
  }
@@ -1747,11 +1789,11 @@ function addGitignoreEnv(projectDir) {
1747
1789
  var AGENT_ARTIFACT_DIRS = [".factory/", ".goose/", ".uzys-agent-harness/"];
1748
1790
  var GITIGNORE_AGENT_ARTIFACT_HEADER = "# agent CLI / harness \uC790\uB3D9 \uC0DD\uC131\uBB3C (auto-added by agent-harness)";
1749
1791
  function addGitignoreAgentArtifacts(projectDir) {
1750
- const path = join9(projectDir, ".gitignore");
1751
- if (!existsSync9(path)) {
1792
+ const path = join10(projectDir, ".gitignore");
1793
+ if (!existsSync10(path)) {
1752
1794
  return [];
1753
1795
  }
1754
- const content = readFileSync8(path, "utf8");
1796
+ const content = readFileSync9(path, "utf8");
1755
1797
  const missing = AGENT_ARTIFACT_DIRS.filter((pattern) => {
1756
1798
  const lineRegex = new RegExp(`^${pattern.replace(/\./g, "\\.").replace(/\//g, "/")}\\s*$`, "m");
1757
1799
  return !lineRegex.test(content);
@@ -1770,10 +1812,14 @@ ${block}
1770
1812
  // src/external-installer.ts
1771
1813
  init_esm_shims();
1772
1814
  import { spawnSync } from "child_process";
1773
- 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";
1774
1816
  import { homedir as homedir2 } from "os";
1775
- import { join as join10 } from "path";
1817
+ import { join as join11 } from "path";
1776
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
+ }
1777
1823
  function selectExternalTargets(assets, ctx) {
1778
1824
  const conditionPassed = filterApplicableAssets(assets, ctx).filter(
1779
1825
  (a) => a.method.kind !== "internal"
@@ -1848,8 +1894,8 @@ function installOne(asset, ctx) {
1848
1894
  cwd
1849
1895
  );
1850
1896
  case "shell-script": {
1851
- const scriptPath = join10(ctx.harnessRoot, method.script);
1852
- if (!existsSync10(scriptPath)) {
1897
+ const scriptPath = join11(ctx.harnessRoot, method.script);
1898
+ if (!existsSync11(scriptPath)) {
1853
1899
  return {
1854
1900
  asset,
1855
1901
  ok: false,
@@ -1945,8 +1991,17 @@ function installPlugin(asset, spawn, method, scope, cwd) {
1945
1991
  );
1946
1992
  }
1947
1993
  function runSpawn(asset, spawn, cmd, args, cwd) {
1948
- const result = spawn(cmd, args, spawnOpts(cwd));
1994
+ const timeout = spawnTimeoutFor(asset.method.kind);
1995
+ const result = spawn(cmd, args, spawnOpts(cwd, timeout));
1949
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
+ }
1950
2005
  return { asset, ok: false, message: result.error.message };
1951
2006
  }
1952
2007
  if ((result.status ?? 1) !== 0) {
@@ -1960,11 +2015,11 @@ function runSpawn(asset, spawn, cmd, args, cwd) {
1960
2015
  }
1961
2016
  return { asset, ok: true };
1962
2017
  }
1963
- function spawnOpts(cwd) {
2018
+ function spawnOpts(cwd, timeout = DEFAULT_SPAWN_TIMEOUT_MS) {
1964
2019
  return {
1965
2020
  encoding: "utf8",
1966
2021
  stdio: "pipe",
1967
- timeout: DEFAULT_SPAWN_TIMEOUT_MS,
2022
+ timeout,
1968
2023
  ...cwd ? { cwd } : {}
1969
2024
  };
1970
2025
  }
@@ -1979,17 +2034,17 @@ function detectVersion(method, spawn) {
1979
2034
  if (at2 <= 0) return void 0;
1980
2035
  const plugin = method.pluginId.slice(0, at2);
1981
2036
  const marketplaceShort = method.pluginId.slice(at2 + 1);
1982
- const cacheBase = join10(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
1983
- if (!existsSync10(cacheBase)) return void 0;
2037
+ const cacheBase = join11(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
2038
+ if (!existsSync11(cacheBase)) return void 0;
1984
2039
  const versions = readdirSync2(cacheBase).filter((v2) => /^\d/.test(v2)).sort();
1985
2040
  return versions.at(-1);
1986
2041
  }
1987
2042
  case "npm": {
1988
2043
  const npmRoot = getNpmGlobalRoot(spawn);
1989
2044
  if (!npmRoot) return void 0;
1990
- const pkgJson = join10(npmRoot, method.pkg, "package.json");
1991
- if (!existsSync10(pkgJson)) return void 0;
1992
- 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"));
1993
2048
  return parsed.version;
1994
2049
  }
1995
2050
  default:
@@ -2016,7 +2071,7 @@ function getNpmGlobalRoot(spawn) {
2016
2071
 
2017
2072
  // src/mcp-merge.ts
2018
2073
  init_esm_shims();
2019
- 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";
2020
2075
  function parseTrackMcpMap(raw) {
2021
2076
  const rows = [];
2022
2077
  for (const line of raw.split(/\r?\n/)) {
@@ -2067,15 +2122,15 @@ function mergeMcpServers(base, rows, tracks) {
2067
2122
  return out;
2068
2123
  }
2069
2124
  function composeMcpJson(opts) {
2070
- const base = JSON.parse(readFileSync10(opts.templateMcpPath, "utf8"));
2071
- const merged = opts.existingPath && existsSync11(opts.existingPath) ? mergeUserBase(base, opts.existingPath) : base;
2072
- 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") : "";
2073
2128
  const rows = parseTrackMcpMap(mapRaw);
2074
2129
  return mergeMcpServers(merged, rows, opts.tracks);
2075
2130
  }
2076
2131
  function mergeUserBase(base, existingPath) {
2077
2132
  try {
2078
- const existing = JSON.parse(readFileSync10(existingPath, "utf8"));
2133
+ const existing = JSON.parse(readFileSync11(existingPath, "utf8"));
2079
2134
  return {
2080
2135
  ...base,
2081
2136
  mcpServers: { ...base.mcpServers, ...existing.mcpServers }
@@ -2093,37 +2148,52 @@ function writeMcpJson(path, mcp) {
2093
2148
  init_esm_shims();
2094
2149
  import {
2095
2150
  copyFileSync,
2096
- existsSync as existsSync12,
2151
+ existsSync as existsSync13,
2097
2152
  mkdirSync as mkdirSync3,
2098
2153
  readdirSync as readdirSync3,
2099
- readFileSync as readFileSync11,
2154
+ readFileSync as readFileSync12,
2155
+ rmdirSync,
2156
+ statSync,
2100
2157
  unlinkSync as unlinkSync2,
2101
2158
  writeFileSync as writeFileSync5
2102
2159
  } from "fs";
2103
- 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";
2104
2161
  var LEGACY_ANCHOR_FILE = ".claude/CLAUDE.md";
2105
- function buildUpdateSpec(projectDir, tracks) {
2162
+ function buildUpdateSpec(projectDir, tracks, only) {
2106
2163
  return {
2107
2164
  tracks: [...tracks],
2108
2165
  options: DEFAULT_OPTIONS,
2109
2166
  cli: ["claude"],
2110
2167
  projectDir,
2111
- 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] } : {}
2112
2171
  };
2113
2172
  }
2114
- function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}) {
2115
- 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);
2116
2182
  const report = {
2117
2183
  updated: {},
2118
2184
  pruned: {},
2119
2185
  staleHookRefs: [],
2120
2186
  claudeMdUpdated: false,
2187
+ backups: [],
2188
+ skippedGroups: only === void 0 ? [] : UPDATE_GROUPS.filter((g) => !only.includes(g)),
2189
+ anchorBackedUp: false,
2121
2190
  anchorCreated: false,
2122
2191
  rootImportAdded: false,
2123
2192
  rootBlockRefreshed: false,
2124
2193
  legacyAnchor: null,
2125
2194
  skillsBackedUp: [],
2126
2195
  skillsSkippedLinks: [],
2196
+ skillsPruned: [],
2127
2197
  policyBackedUp: [],
2128
2198
  externalUpdated: 0,
2129
2199
  externalBackedUp: [],
@@ -2136,16 +2206,18 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}) {
2136
2206
  restored: [],
2137
2207
  needsReinstall: [],
2138
2208
  retiredAgents: [],
2209
+ demotedAgents: [],
2139
2210
  mcpAllowlistRetired: null
2140
2211
  };
2141
- const fresh = installNewAssets(projectDir, templatesDir);
2212
+ const fresh = installNewAssets(projectDir, templatesDir, wants);
2142
2213
  report.installedNew = fresh.installed;
2143
2214
  report.restored = fresh.restored;
2144
2215
  report.needsReinstall = fresh.needsReinstall;
2145
2216
  const policyBase = policyBaseline(projectDir);
2146
2217
  for (const { dir, ext } of POLICY_DIRS) {
2147
- const target = join11(claudeDir, dir);
2148
- 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);
2149
2221
  const label = `.claude/${dir}`;
2150
2222
  const ctx = { prefix: dir, baseline: policyBase };
2151
2223
  const synced = updateDir(target, source, ext, ctx);
@@ -2153,29 +2225,38 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}) {
2153
2225
  report.policyBackedUp.push(...synced.backedUp);
2154
2226
  report.pruned[label] = pruneOrphans(target, source, ext, ctx);
2155
2227
  }
2156
- refreshPolicyBaseline(projectDir, templatesDir);
2157
- const skillSync = syncSkills(
2158
- join11(claudeDir, "skills"),
2159
- 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"),
2160
2235
  skillBaseline(projectDir),
2161
2236
  /* @__PURE__ */ new Date(),
2162
2237
  (relInSkills) => foreignOwnedTarget(projectDir, `.claude/skills/${relInSkills}`)
2163
- );
2164
- report.updated[".claude/skills"] = skillSync.updated;
2238
+ ) : { updated: 0, backedUp: [], skippedLinks: [], foreignOwned: [], pruned: [] };
2239
+ if (wants("skills")) report.updated[".claude/skills"] = skillSync.updated;
2165
2240
  report.skillsBackedUp = skillSync.backedUp;
2166
2241
  report.skillsSkippedLinks = skillSync.skippedLinks;
2167
- 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);
2168
2248
  const installLog = readInstallLog(projectDir);
2169
- if (existsSync12(claudeDir) && (installLog === null || installLog.spec.cli.includes("claude"))) {
2170
- syncHarnessAnchor(projectDir, templatesDir, report);
2249
+ if (existsSync13(claudeDir) && (installLog === null || installLog.spec.cli.includes("claude"))) {
2250
+ if (wants("anchor")) syncHarnessAnchor(projectDir, templatesDir, report);
2171
2251
  }
2172
- const settingsPath = join11(claudeDir, "settings.json");
2173
- if (existsSync12(settingsPath)) {
2252
+ const settingsPath = join12(claudeDir, "settings.json");
2253
+ if (wants("hooks") && existsSync13(settingsPath)) {
2174
2254
  report.staleHookRefs = cleanStaleHookRefs(settingsPath, claudeDir);
2175
2255
  }
2176
- report.mcpAllowlistRetired = retireMcpAllowlist(projectDir);
2256
+ if (wants("hooks")) report.mcpAllowlistRetired = retireMcpAllowlist(projectDir);
2177
2257
  report.retiredAgents = staleAgentFiles(claudeDir);
2178
- const external = refreshExternalCli(projectDir, harnessRoot);
2258
+ report.demotedAgents = demotedAgentFiles(claudeDir, installedTracks(projectDir));
2259
+ const external = wants("external") ? refreshExternalCli(projectDir, harnessRoot) : { externalUpdated: 0, externalBackedUp: [], externalForeignOwned: [] };
2179
2260
  report.externalUpdated = external.externalUpdated;
2180
2261
  report.externalBackedUp = external.externalBackedUp;
2181
2262
  const saidBySlotRow = new Set(report.skillsSkippedLinks.map((id) => `.claude/skills/${id}`));
@@ -2183,12 +2264,13 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}) {
2183
2264
  for (const f of [
2184
2265
  ...skillSync.foreignOwned,
2185
2266
  ...fresh.foreignOwned,
2267
+ ...newSkills.foreignOwned,
2186
2268
  ...external.externalForeignOwned
2187
2269
  ]) {
2188
2270
  if (!saidBySlotRow.has(f) && !merged.includes(f)) merged.push(f);
2189
2271
  }
2190
2272
  report.foreignOwned = merged;
2191
- const skillRefresh = (deps.refreshSkills ?? refreshExternalSkills)(projectDir);
2273
+ const skillRefresh = wants("external") ? (deps.refreshSkills ?? refreshExternalSkills)(projectDir) : { attempted: 0, refreshed: 0, failed: [], notInCatalog: [], unknown: false };
2192
2274
  report.externalSkillsRefreshed = skillRefresh.refreshed;
2193
2275
  report.externalSkillsFailed = skillRefresh.failed;
2194
2276
  report.externalSkillsNotInCatalog = [
@@ -2196,20 +2278,31 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}) {
2196
2278
  ...staleSkillDirs(claudeDir).filter((id) => !skillRefresh.notInCatalog.includes(id))
2197
2279
  ];
2198
2280
  report.externalSkillsUnknown = skillRefresh.unknown;
2281
+ report.backups = collectRunBackups(projectDir, startedAt);
2282
+ writeBackupList(projectDir, report.backups);
2199
2283
  return report;
2200
2284
  }
2201
2285
  function staleSkillDirs(claudeDir) {
2202
- const dir = join11(claudeDir, "skills");
2203
- if (!existsSync12(dir)) return [];
2286
+ const dir = join12(claudeDir, "skills");
2287
+ if (!existsSync13(dir)) return [];
2204
2288
  const stale = (id) => RENAMED_SKILL_IDS[id] !== void 0 || RETIRED_SKILL_IDS.includes(id);
2205
2289
  return readdirSync3(dir, { withFileTypes: true }).filter((e2) => e2.isDirectory() && stale(e2.name)).map((e2) => e2.name);
2206
2290
  }
2207
2291
  function staleAgentFiles(claudeDir) {
2208
- const dir = join11(claudeDir, "agents");
2209
- if (!existsSync12(dir)) return [];
2292
+ const dir = join12(claudeDir, "agents");
2293
+ if (!existsSync13(dir)) return [];
2210
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));
2211
2295
  }
2212
- function installNewAssets(projectDir, templatesDir) {
2296
+ function demotedAgentFiles(claudeDir, tracks) {
2297
+ if (tracks.length === 0) return [];
2298
+ const dir = join12(claudeDir, "agents");
2299
+ if (!existsSync13(dir)) return [];
2300
+ const demoted = TRACK_AGENTS.filter(([, pattern]) => !anyTrack(tracks, pattern)).map(
2301
+ ([id]) => id
2302
+ );
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));
2304
+ }
2305
+ function installNewAssets(projectDir, templatesDir, wants = () => true) {
2213
2306
  const installed = [];
2214
2307
  const restored = [];
2215
2308
  const needsReinstall = [];
@@ -2219,17 +2312,19 @@ function installNewAssets(projectDir, templatesDir) {
2219
2312
  const baselineExcluded = new Set(log?.spec.baselineExclude ?? []);
2220
2313
  const priorBaseline = policyBaseline(projectDir);
2221
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;
2222
2317
  if (entry.target === HARNESS_ANCHOR_FILE) continue;
2223
2318
  if (isBaselineExcluded(entry.target, baselineExcluded)) continue;
2224
- const target = join11(projectDir, entry.target);
2319
+ const target = join12(projectDir, entry.target);
2225
2320
  const foreign = foreignOwnedTarget(projectDir, entry.target);
2226
2321
  if (foreign !== null) {
2227
2322
  if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
2228
2323
  continue;
2229
2324
  }
2230
- if (existsSync12(target)) continue;
2231
- const source = join11(templatesDir, entry.source);
2232
- if (!existsSync12(source)) continue;
2325
+ if (existsSync13(target)) continue;
2326
+ const source = join12(templatesDir, entry.source);
2327
+ if (!existsSync13(source)) continue;
2233
2328
  if (entry.target.startsWith(".claude/")) {
2234
2329
  if (!claudeSelected) continue;
2235
2330
  if (entry.target.startsWith(".claude/hooks/") || entry.target === ".claude/settings.json") {
@@ -2245,6 +2340,77 @@ function installNewAssets(projectDir, templatesDir) {
2245
2340
  }
2246
2341
  return { installed, restored, needsReinstall, foreignOwned };
2247
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
+ }
2248
2414
  function trackOnlyFileAssets(tracks) {
2249
2415
  const base = { tracks, selectedInternalSkills: [] };
2250
2416
  const optedOut = { ...base, withEcc: false, withTauri: false };
@@ -2253,34 +2419,45 @@ function trackOnlyFileAssets(tracks) {
2253
2419
  (e2) => e2.type === "file" && e2.applies(optedOut) && e2.applies(optedIn)
2254
2420
  );
2255
2421
  }
2256
- function syncHarnessAnchor(projectDir, templatesDir, report) {
2257
- 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))) {
2258
2424
  report.legacyAnchor = LEGACY_ANCHOR_FILE;
2259
2425
  }
2260
- const templateMd = join11(templatesDir, "CLAUDE.md");
2261
- if (!existsSync12(templateMd)) return;
2262
- const anchor = join11(projectDir, HARNESS_ANCHOR_FILE);
2263
- const existed = existsSync12(anchor);
2264
- 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);
2265
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
+ }
2266
2441
  report.claudeMdUpdated = true;
2442
+ recordAnchorBaseline(projectDir, anchor);
2267
2443
  report.rootBlockRefreshed = upsertRootImport(projectDir);
2268
2444
  return;
2269
2445
  }
2446
+ copyFileSync(templateMd, anchor);
2270
2447
  report.anchorCreated = true;
2271
2448
  report.rootImportAdded = upsertRootImport(projectDir);
2272
2449
  recordAnchorBaseline(projectDir, anchor);
2273
2450
  }
2274
2451
  function upsertRootImport(projectDir) {
2275
- const target = join11(projectDir, "CLAUDE.md");
2276
- const existing = existsSync12(target) ? readFileSync11(target, "utf8") : null;
2452
+ const target = join12(projectDir, "CLAUDE.md");
2453
+ const existing = existsSync13(target) ? readFileSync12(target, "utf8") : null;
2277
2454
  const next = upsertHarnessImport(existing, {
2278
2455
  projectName: basename4(projectDir),
2279
2456
  tracks: installedTracks(projectDir),
2280
2457
  // ADR-085 — update 는 선택 목록을 인자로 받지 않는다. `syncSkills` 와 같은 판정을 쓴다:
2281
2458
  // `.claude/skills/<id>` 가 있으면 그 스킬은 이 설치가 고른 것이다.
2282
2459
  continuousSkills: CONTINUOUS_SKILLS.map((s) => s.id).filter(
2283
- (id) => existsSync12(join11(projectDir, ".claude/skills", id))
2460
+ (id) => existsSync13(join12(projectDir, ".claude/skills", id))
2284
2461
  )
2285
2462
  });
2286
2463
  if (next === existing) return false;
@@ -2300,7 +2477,7 @@ function recordAnchorBaseline(projectDir, anchor) {
2300
2477
  ...log.templates,
2301
2478
  rootClaudeMd: {
2302
2479
  path: HARNESS_ANCHOR_FILE,
2303
- sha256: hashContent(readFileSync11(anchor, "utf8"))
2480
+ sha256: hashContent(readFileSync12(anchor, "utf8"))
2304
2481
  }
2305
2482
  }
2306
2483
  };
@@ -2342,16 +2519,16 @@ function isHarnessOwned2(ctx, rel, current) {
2342
2519
  return isHarnessOwned(ctx.baseline, `${ctx.prefix}/${rel}`, current);
2343
2520
  }
2344
2521
  function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2345
- if (!existsSync12(target) || !existsSync12(source)) return { updated: 0, backedUp: [] };
2522
+ if (!existsSync13(target) || !existsSync13(source)) return { updated: 0, backedUp: [] };
2346
2523
  let updated = 0;
2347
2524
  const backedUp = [];
2348
2525
  for (const file of readdirSync3(target)) {
2349
2526
  if (!file.endsWith(ext)) continue;
2350
- const targetFile = join11(target, file);
2351
- const sourceFile = join11(source, file);
2352
- if (!existsSync12(sourceFile)) continue;
2353
- const next = readFileSync11(sourceFile, "utf8");
2354
- 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");
2355
2532
  if (current === next) continue;
2356
2533
  if (!isHarnessOwned2(ctx, file, current)) {
2357
2534
  backupFile(targetFile, now);
@@ -2363,35 +2540,36 @@ function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2363
2540
  return { updated, backedUp };
2364
2541
  }
2365
2542
  function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Date(), foreignOf) {
2366
- if (!existsSync12(targetDir) || !existsSync12(sourceDir))
2367
- return { updated: 0, backedUp: [], skippedLinks: [], foreignOwned: [] };
2543
+ if (!existsSync13(targetDir) || !existsSync13(sourceDir))
2544
+ return { updated: 0, backedUp: [], skippedLinks: [], foreignOwned: [], pruned: [] };
2368
2545
  let updated = 0;
2369
2546
  const backedUp = [];
2370
2547
  const skippedLinks = [];
2371
2548
  const foreignOwned = [];
2549
+ const pruned = [];
2372
2550
  for (const skill of readdirSync3(sourceDir, { withFileTypes: true })) {
2373
2551
  if (!skill.isDirectory()) continue;
2374
- const targetSkill = join11(targetDir, skill.name);
2552
+ const targetSkill = join12(targetDir, skill.name);
2375
2553
  if (occupiedByNonDirectory(targetSkill)) {
2376
2554
  skippedLinks.push(skill.name);
2377
2555
  continue;
2378
2556
  }
2379
- if (!existsSync12(targetSkill)) continue;
2380
- for (const rel of listFilesRecursive(join11(sourceDir, skill.name))) {
2381
- 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);
2382
2560
  const foreign = foreignOf(`${skill.name}/${rel}`);
2383
2561
  if (foreign !== null) {
2384
2562
  if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
2385
2563
  continue;
2386
2564
  }
2387
- const next = readFileSync11(join11(sourceDir, skill.name, rel), "utf8");
2388
- if (!existsSync12(targetFile)) {
2565
+ const next = readFileSync12(join12(sourceDir, skill.name, rel), "utf8");
2566
+ if (!existsSync13(targetFile)) {
2389
2567
  mkdirSync3(dirname3(targetFile), { recursive: true });
2390
2568
  writeFileSync5(targetFile, next);
2391
2569
  updated++;
2392
2570
  continue;
2393
2571
  }
2394
- const current = readFileSync11(targetFile, "utf8");
2572
+ const current = readFileSync12(targetFile, "utf8");
2395
2573
  if (current === next) continue;
2396
2574
  const recorded = baseline.get(`${skill.name}/${rel}`);
2397
2575
  if (recorded === void 0 || hashContent(current) !== recorded) {
@@ -2401,8 +2579,23 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
2401
2579
  writeFileSync5(targetFile, next);
2402
2580
  updated++;
2403
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
+ }
2404
2597
  }
2405
- return { updated, backedUp, skippedLinks, foreignOwned };
2598
+ return { updated, backedUp, skippedLinks, foreignOwned, pruned };
2406
2599
  }
2407
2600
  function skillBaseline(projectDir) {
2408
2601
  const log = readInstallLog(projectDir);
@@ -2412,10 +2605,14 @@ function policyBaseline(projectDir) {
2412
2605
  const log = readInstallLog(projectDir);
2413
2606
  return new Map((log?.policyFiles ?? []).map((f) => [f.path, f.sha256]));
2414
2607
  }
2415
- function refreshPolicyBaseline(projectDir, templatesDir) {
2608
+ function refreshPolicyBaseline(projectDir, templatesDir, dirs) {
2416
2609
  const log = readInstallLog(projectDir);
2417
2610
  if (!log) return;
2418
- 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
+ ];
2419
2616
  const next = { ...log };
2420
2617
  if (policyFiles.length > 0) next.policyFiles = policyFiles;
2421
2618
  else delete next.policyFiles;
@@ -2424,10 +2621,10 @@ function refreshPolicyBaseline(projectDir, templatesDir) {
2424
2621
  } catch {
2425
2622
  }
2426
2623
  }
2427
- function refreshSkillBaseline(projectDir) {
2624
+ function refreshSkillBaseline(projectDir, templatesDir) {
2428
2625
  const log = readInstallLog(projectDir);
2429
2626
  if (!log) return;
2430
- const skillFiles = collectSkillHashes(projectDir);
2627
+ const skillFiles = collectSkillHashes(projectDir, templatesDir);
2431
2628
  const next = { ...log };
2432
2629
  if (skillFiles.length > 0) next.skillFiles = skillFiles;
2433
2630
  else delete next.skillFiles;
@@ -2437,16 +2634,16 @@ function refreshSkillBaseline(projectDir) {
2437
2634
  }
2438
2635
  }
2439
2636
  function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2440
- if (!existsSync12(target) || !existsSync12(source)) return [];
2637
+ if (!existsSync13(target) || !existsSync13(source)) return [];
2441
2638
  const removed = [];
2442
2639
  for (const file of readdirSync3(target)) {
2443
2640
  if (!file.endsWith(ext)) continue;
2444
- const sourceFile = join11(source, file);
2445
- if (!existsSync12(sourceFile)) {
2446
- const targetFile = join11(target, file);
2641
+ const sourceFile = join12(source, file);
2642
+ if (!existsSync13(sourceFile)) {
2643
+ const targetFile = join12(target, file);
2447
2644
  if (!ctx.baseline.has(`${ctx.prefix}/${file}`)) continue;
2448
2645
  try {
2449
- const current = readFileSync11(targetFile, "utf8");
2646
+ const current = readFileSync12(targetFile, "utf8");
2450
2647
  if (!isHarnessOwned2(ctx, file, current)) backupFile(targetFile, now);
2451
2648
  unlinkSync2(targetFile);
2452
2649
  removed.push(file);
@@ -2457,8 +2654,8 @@ function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()
2457
2654
  return removed;
2458
2655
  }
2459
2656
  function retireMcpAllowlist(projectDir, now = /* @__PURE__ */ new Date()) {
2460
- const target = join11(projectDir, ".mcp-allowlist");
2461
- if (!existsSync12(target)) return null;
2657
+ const target = join12(projectDir, ".mcp-allowlist");
2658
+ if (!existsSync13(target)) return null;
2462
2659
  try {
2463
2660
  const backup = backupFile(target, now);
2464
2661
  unlinkSync2(target);
@@ -2470,7 +2667,7 @@ function retireMcpAllowlist(projectDir, now = /* @__PURE__ */ new Date()) {
2470
2667
  function cleanStaleHookRefs(settingsPath, claudeDir) {
2471
2668
  let settings;
2472
2669
  try {
2473
- settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
2670
+ settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
2474
2671
  } catch {
2475
2672
  return [];
2476
2673
  }
@@ -2497,7 +2694,7 @@ function cleanStaleHookRefs(settingsPath, claudeDir) {
2497
2694
  function keepHookRef(hook, claudeDir, removed) {
2498
2695
  const relPath = projectAnchoredRef(hook?.command ?? "", claudeDir);
2499
2696
  if (relPath === void 0) return true;
2500
- const exists = existsSync12(join11(claudeDir, relPath));
2697
+ const exists = existsSync13(join12(claudeDir, relPath));
2501
2698
  if (!exists && !removed.includes(relPath)) removed.push(relPath);
2502
2699
  return exists;
2503
2700
  }
@@ -2524,14 +2721,14 @@ function projectAnchoredRef(command, claudeDir) {
2524
2721
  function runInstall(ctx) {
2525
2722
  const { harnessRoot, projectDir, spec } = ctx;
2526
2723
  const mode = ctx.mode ?? "fresh";
2527
- const templatesDir = join12(harnessRoot, "templates");
2528
- if (!existsSync13(templatesDir)) {
2724
+ const templatesDir = join13(harnessRoot, "templates");
2725
+ if (!existsSync14(templatesDir)) {
2529
2726
  throw new Error(`Templates dir not found: ${templatesDir}`);
2530
2727
  }
2531
- const claudeDir = join12(projectDir, ".claude");
2728
+ const claudeDir = join13(projectDir, ".claude");
2532
2729
  const previousLog = readInstallLog(projectDir);
2533
2730
  const claudeWasSelected = previousLog?.spec.cli.includes("claude") ?? false;
2534
- if (mode === "update" && !existsSync13(claudeDir) && (previousLog === null || claudeWasSelected)) {
2731
+ if (mode === "update" && !existsSync14(claudeDir) && (previousLog === null || claudeWasSelected)) {
2535
2732
  throw new Error(
2536
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}`
2537
2734
  );
@@ -2616,9 +2813,9 @@ function runInstall(ctx) {
2616
2813
  }
2617
2814
  function healStaleHookRefs(spec, projectDir) {
2618
2815
  if (!spec.cli.includes("claude")) return [];
2619
- const settingsPath = join12(projectDir, ".claude/settings.json");
2620
- if (!existsSync13(settingsPath)) return [];
2621
- 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"));
2622
2819
  }
2623
2820
  function resolveBackupPath(ctx, mode, claudeDir) {
2624
2821
  const wantBackup = ctx.backup ?? (mode === "update" || mode === "reinstall");
@@ -2626,7 +2823,13 @@ function resolveBackupPath(ctx, mode, claudeDir) {
2626
2823
  return mode === "update" ? copyBackupDir(claudeDir) : backupDir(claudeDir);
2627
2824
  }
2628
2825
  function runUpdateInstall(ctx, templatesDir, backupPath) {
2629
- 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
+ );
2630
2833
  const baseline = {
2631
2834
  filesCopied: 0,
2632
2835
  dirsCopied: 0,
@@ -2684,12 +2887,12 @@ function installCliNeutralAssets(manifestSpec, projectDir, templatesDir, baselin
2684
2887
  continue;
2685
2888
  }
2686
2889
  if (!isCliNeutralTarget(entry.target)) continue;
2687
- const source = join12(templatesDir, entry.source);
2688
- if (!existsSync13(source)) {
2890
+ const source = join13(templatesDir, entry.source);
2891
+ if (!existsSync14(source)) {
2689
2892
  result.skipped += 1;
2690
2893
  continue;
2691
2894
  }
2692
- copyFile(source, join12(projectDir, entry.target));
2895
+ copyFile(source, join13(projectDir, entry.target));
2693
2896
  result.filesCopied += 1;
2694
2897
  }
2695
2898
  return result;
@@ -2701,9 +2904,9 @@ function backupEditedPolicyFile(entryTarget, target, source, baseline) {
2701
2904
  if (!POLICY_DIRS.some(({ dir, ext }) => rel.startsWith(`${dir}/`) && rel.endsWith(ext))) {
2702
2905
  return null;
2703
2906
  }
2704
- if (!existsSync13(target)) return null;
2705
- const current = readFileSync12(target, "utf-8");
2706
- 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;
2707
2910
  const recorded = baseline.get(rel);
2708
2911
  if (recorded !== void 0 && recorded === hashContent(current)) return null;
2709
2912
  return backupFile(target);
@@ -2716,14 +2919,14 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
2716
2919
  if (!entry.applies(manifestSpec)) {
2717
2920
  continue;
2718
2921
  }
2719
- const target = join12(projectDir, entry.target);
2922
+ const target = join13(projectDir, entry.target);
2720
2923
  if (isBaselineExcluded(entry.target, baselineExcluded)) {
2721
2924
  result.excluded.push(entry.target);
2722
- if (existsSync13(target)) result.excludedOnDisk.push(entry.target);
2925
+ if (existsSync14(target)) result.excludedOnDisk.push(entry.target);
2723
2926
  continue;
2724
2927
  }
2725
- const source = join12(templatesDir, entry.source);
2726
- if (!existsSync13(source)) {
2928
+ const source = join13(templatesDir, entry.source);
2929
+ if (!existsSync14(source)) {
2727
2930
  result.skipped += 1;
2728
2931
  continue;
2729
2932
  }
@@ -2734,7 +2937,7 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
2734
2937
  }
2735
2938
  if (entry.type === "file") {
2736
2939
  if (entry.target === ".claude/settings.json") {
2737
- const backup = backupFileIfChanged(target, readFileSync12(source, "utf-8"));
2940
+ const backup = backupFileIfChanged(target, readFileSync13(source, "utf-8"));
2738
2941
  if (backup) {
2739
2942
  result.backups.push(backup);
2740
2943
  }
@@ -2758,8 +2961,8 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
2758
2961
  }
2759
2962
  accumulateCategory(result.categories, entry);
2760
2963
  }
2761
- const hookDir = join12(projectDir, ".claude/hooks");
2762
- if (existsSync13(hookDir)) {
2964
+ const hookDir = join13(projectDir, ".claude/hooks");
2965
+ if (existsSync14(hookDir)) {
2763
2966
  chmodHooksSync(hookDir);
2764
2967
  }
2765
2968
  writeInstalledTracks(projectDir, manifestSpec.tracks);
@@ -2773,9 +2976,9 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
2773
2976
  return result;
2774
2977
  }
2775
2978
  function harnessAnchorLog(projectDir) {
2776
- const anchor = join12(projectDir, HARNESS_ANCHOR_FILE);
2777
- if (!existsSync13(anchor)) return null;
2778
- 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")) };
2779
2982
  }
2780
2983
  function writeEnvironmentFiles(projectDir, tracks) {
2781
2984
  return {
@@ -2833,8 +3036,8 @@ function writeInstallLogSafe(ctx, externalFiles, external, rootClaudeMdLog, prev
2833
3036
  claudeDirMovedAside,
2834
3037
  rootFiles
2835
3038
  );
2836
- const skillFiles = collectSkillHashes(ctx.projectDir);
2837
- 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"));
2838
3041
  const merged = mergeExternalFiles(ctx.projectDir, previousLog?.externalFiles, externalFiles);
2839
3042
  writeInstallLog(ctx.projectDir, {
2840
3043
  ...log,
@@ -2850,11 +3053,11 @@ function writeInstallLogSafe(ctx, externalFiles, external, rootClaudeMdLog, prev
2850
3053
  }
2851
3054
  }
2852
3055
  function composeAndWriteMcp(harnessRoot, projectDir, spec) {
2853
- const mcpPath = join12(projectDir, ".mcp.json");
2854
- const created = !existsSync13(mcpPath);
3056
+ const mcpPath = join13(projectDir, ".mcp.json");
3057
+ const created = !existsSync14(mcpPath);
2855
3058
  const composed = composeMcpJson({
2856
- templateMcpPath: join12(harnessRoot, "templates/mcp.json"),
2857
- trackMapPath: join12(harnessRoot, "templates/track-mcp-map.tsv"),
3059
+ templateMcpPath: join13(harnessRoot, "templates/mcp.json"),
3060
+ trackMapPath: join13(harnessRoot, "templates/track-mcp-map.tsv"),
2858
3061
  existingPath: mcpPath,
2859
3062
  tracks: spec.tracks
2860
3063
  });
@@ -2907,15 +3110,15 @@ function accumulateCategory(cats, entry) {
2907
3110
  }
2908
3111
  }
2909
3112
  function writeInstalledTracks(projectDir, tracks) {
2910
- const path = join12(projectDir, ".claude/.installed-tracks");
3113
+ const path = join13(projectDir, ".claude/.installed-tracks");
2911
3114
  mkdirSync4(dirname4(path), { recursive: true });
2912
3115
  const sorted = [...new Set(tracks)].sort().join("\n");
2913
3116
  writeFileSync6(path, `${sorted}
2914
3117
  `);
2915
3118
  }
2916
3119
  function writeRootClaudeMd(projectDir, tracks, continuousSkills) {
2917
- const target = join12(projectDir, "CLAUDE.md");
2918
- 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;
2919
3122
  const content = upsertHarnessImport(existing, {
2920
3123
  projectName: basename5(projectDir),
2921
3124
  tracks,
@@ -2940,6 +3143,8 @@ function listHookFiles(hookDir) {
2940
3143
 
2941
3144
  // src/commands/install-render.ts
2942
3145
  init_esm_shims();
3146
+ import { existsSync as existsSync15, readdirSync as readdirSync5 } from "fs";
3147
+ import { join as join14 } from "path";
2943
3148
 
2944
3149
  // src/preset-recommend.ts
2945
3150
  init_esm_shims();
@@ -3005,12 +3210,18 @@ function renderInstallHeader(log, spec, mode) {
3005
3210
  for (const [cat, ids] of groupAssetsByCategory(finalAssets)) {
3006
3211
  log(` ${c.dim(`\xB7 ${cat}:`)} ${ids.join(", ")}`);
3007
3212
  }
3008
- const assetSpec = buildManifestSpec(spec);
3009
- const cost = formatResidentCostLine(
3010
- residentCost(buildManifest(assetSpec).filter((e2) => e2.applies(assetSpec))),
3011
- summarizeContextCost(finalAssets).unmeasuredCount
3012
- );
3013
- if (cost) log(` ${c.dim(`\xB7 ${cost}`)}`);
3213
+ if (mode !== "update") {
3214
+ const assetSpec = buildManifestSpec(spec);
3215
+ const cost = formatResidentCostLine(
3216
+ residentCost(
3217
+ buildManifest(assetSpec).filter(
3218
+ (e2) => e2.applies(assetSpec) && landsOnDisk(e2.target, spec.cli)
3219
+ )
3220
+ ),
3221
+ summarizeContextCost(finalAssets).unmeasuredCount
3222
+ );
3223
+ if (cost) log(` ${c.dim(`\xB7 ${cost}`)}`);
3224
+ }
3014
3225
  }
3015
3226
  log("");
3016
3227
  }
@@ -3055,6 +3266,10 @@ function createInstallRenderer(log, spec, verbose) {
3055
3266
  log(` ${c.bold(`\u2501\u2501 ${CATEGORY_TITLES[asset.category]} \u2501\u2501`)}`);
3056
3267
  currentCategory = asset.category;
3057
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
+ }
3058
3273
  },
3059
3274
  onAssetResult: (result) => {
3060
3275
  const meta = result.ok ? formatAssetMeta(result.asset, result.version) : result.message ?? "failed";
@@ -3064,7 +3279,7 @@ function createInstallRenderer(log, spec, verbose) {
3064
3279
  };
3065
3280
  return { callbacks, phase2HeaderPrinted: () => phase2HeaderPrinted };
3066
3281
  }
3067
- function renderUpdateSummary(log, report) {
3282
+ function renderUpdateSummary(log, spec, report) {
3068
3283
  log("");
3069
3284
  log(unifiedSection("Summary"));
3070
3285
  log("");
@@ -3074,8 +3289,41 @@ function renderUpdateSummary(log, report) {
3074
3289
  log(infoRow("BACKUP", shortenPath(report.backup)));
3075
3290
  log(infoRow("ROLLBACK", `rm -rf .claude && mv ${shortenPath(report.backup)} .claude`));
3076
3291
  }
3292
+ const cost = formatResidentCostLine(
3293
+ residentCost(residentEntriesOnDisk(spec)),
3294
+ summarizeContextCost(finalSelectedAssets(spec.tracks, spec.userOverride)).unmeasuredCount
3295
+ );
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
+ }
3077
3312
  log("");
3078
3313
  }
3314
+ function residentEntriesOnDisk(spec) {
3315
+ const assetSpec = buildManifestSpec(spec);
3316
+ const planned = buildManifest(assetSpec).filter((e2) => e2.applies(assetSpec));
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) => ({
3319
+ source: `agents/${e2.name}`,
3320
+ target: `.claude/agents/${e2.name}`,
3321
+ // 배포판이 아니라 **이 프로젝트의 파일**을 잰다 — 사용자가 고친 descriptor 도,
3322
+ // 배포판에 더는 없는 에이전트도 실제로 상주하는 것은 이쪽이다.
3323
+ file: join14(agentsDir, e2.name)
3324
+ })) : [];
3325
+ return [...planned.filter((e2) => !e2.target.startsWith(".claude/agents/")), ...onDisk];
3326
+ }
3079
3327
  function renderCliArtifacts(log, spec, report) {
3080
3328
  const hasArtifacts = Boolean(report.codex || report.opencode || report.antigravity);
3081
3329
  const cliSelected = targetsInclude(spec.cli, "codex") || targetsInclude(spec.cli, "opencode") || targetsInclude(spec.cli, "antigravity");
@@ -3280,9 +3528,27 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
3280
3528
  log(assetRow("skip", `${dir} orphan prune`, `${removed.length} removed`));
3281
3529
  }
3282
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
+ }
3283
3540
  if (baseline.updateMode.claudeMdUpdated) {
3284
3541
  log(assetRow("success", HARNESS_ANCHOR_FILE, "refreshed from template"));
3285
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
+ }
3286
3552
  if (baseline.updateMode.anchorCreated) {
3287
3553
  log(assetRow("success", HARNESS_ANCHOR_FILE, "created from template (anchor migration)"));
3288
3554
  }
@@ -3316,6 +3582,15 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
3316
3582
  )
3317
3583
  );
3318
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
+ }
3319
3594
  if (baseline.updateMode.skillsSkippedLinks.length > 0) {
3320
3595
  log(
3321
3596
  assetRow(
@@ -3353,6 +3628,16 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
3353
3628
  )
3354
3629
  );
3355
3630
  }
3631
+ for (const [id, pattern] of TRACK_AGENTS) {
3632
+ if (!baseline.updateMode.demotedAgents.includes(id)) continue;
3633
+ log(
3634
+ assetRow(
3635
+ "skip",
3636
+ "agents",
3637
+ `${id} \xB7 \uC774 \uD2B8\uB799\uC5D0\uC11C\uB294 \uB354 \uC774\uC0C1 \uC124\uCE58\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 ${pattern.split("|").join(" \xB7 ")} \uD2B8\uB799 \uC804\uC6A9 \xB7 .claude/agents/${id}.md \uB97C \uC9C0\uC6CC\uB3C4 \uB41C\uB2E4`
3638
+ )
3639
+ );
3640
+ }
3356
3641
  if (baseline.updateMode.policyBackedUp.length > 0) {
3357
3642
  log(
3358
3643
  assetRow(
@@ -3735,7 +4020,7 @@ function executeSpec(spec, deps = {}) {
3735
4020
  return;
3736
4021
  }
3737
4022
  if (report.updateMode) {
3738
- renderUpdateSummary(log, report);
4023
+ renderUpdateSummary(log, spec, report);
3739
4024
  return;
3740
4025
  }
3741
4026
  if (renderer.phase2HeaderPrinted()) {
@@ -3800,8 +4085,8 @@ function registerInstallCommand(cli2) {
3800
4085
 
3801
4086
  // src/commands/list.ts
3802
4087
  init_esm_shims();
3803
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
3804
- import { join as join13, 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";
3805
4090
  function listAction(options = {}, deps = {}) {
3806
4091
  const log = deps.log ?? console.log;
3807
4092
  const err = deps.err ?? console.error;
@@ -3864,8 +4149,8 @@ function formatTemplateRows(log, projectDir) {
3864
4149
  const rows = [` ${c.dim(dirs.join(" "))}`];
3865
4150
  const rootMd = log.templates.rootClaudeMd;
3866
4151
  if (rootMd) {
3867
- const path = join13(projectDir, rootMd.path);
3868
- const modified = existsSync14(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;
3869
4154
  rows.push(
3870
4155
  modified ? ` ${c.dim(rootMd.path)} ${c.yellow("(\uC218\uC815\uB428 \u2014 uninstall \uC2DC \uBCF4\uC874)")}` : ` ${c.dim(rootMd.path)}`
3871
4156
  );
@@ -3873,7 +4158,7 @@ function formatTemplateRows(log, projectDir) {
3873
4158
  return rows;
3874
4159
  }
3875
4160
  function formatRootFileRows(rootFiles, projectDir) {
3876
- const present = rootFiles.filter((f) => existsSync14(join13(projectDir, f.path)));
4161
+ const present = rootFiles.filter((f) => existsSync16(join15(projectDir, f.path)));
3877
4162
  const width = Math.max(0, ...present.map((f) => f.path.length));
3878
4163
  return present.map(
3879
4164
  (f) => ` ${c.dim(padDisplay(f.path, width))} ${c.dim(f.change === "created" ? "\uC0DD\uC131" : "\uBCD1\uD569")} ${c.dim(f.notes.join(" / "))}`
@@ -3891,15 +4176,15 @@ function registerListCommand(cli2) {
3891
4176
  init_esm_shims();
3892
4177
  import { spawnSync as spawnSync2 } from "child_process";
3893
4178
  import {
3894
- existsSync as existsSync15,
4179
+ existsSync as existsSync17,
3895
4180
  lstatSync as lstatSync2,
3896
- readdirSync as readdirSync5,
3897
- readFileSync as readFileSync14,
3898
- rmdirSync,
4181
+ readdirSync as readdirSync6,
4182
+ readFileSync as readFileSync15,
4183
+ rmdirSync as rmdirSync2,
3899
4184
  rmSync,
3900
4185
  writeFileSync as writeFileSync7
3901
4186
  } from "fs";
3902
- import { dirname as dirname5, join as join14, 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";
3903
4188
 
3904
4189
  // src/uninstall-interactive.ts
3905
4190
  init_esm_shims();
@@ -5139,7 +5424,7 @@ function settleLog(ctx, io) {
5139
5424
  return true;
5140
5425
  }
5141
5426
  } else {
5142
- io.rm(join14(projectDir, INSTALL_LOG_DIR));
5427
+ io.rm(join16(projectDir, INSTALL_LOG_DIR));
5143
5428
  io.rm(legacyInstallLogPath(projectDir));
5144
5429
  io.log(` ${status.success("install log removed")}`);
5145
5430
  }
@@ -5189,7 +5474,7 @@ function advisoryLines(plan, projectDir, rootFiles) {
5189
5474
  return lines;
5190
5475
  }
5191
5476
  function rootFileAdvisoryLines(rootFiles, projectDir) {
5192
- const present = rootFiles.filter((f) => existsSync15(join14(projectDir, f.path)));
5477
+ const present = rootFiles.filter((f) => existsSync17(join16(projectDir, f.path)));
5193
5478
  if (present.length === 0) return [];
5194
5479
  return [
5195
5480
  "",
@@ -5290,16 +5575,16 @@ function buildGlobalAdvisoryCmd(asset) {
5290
5575
  }
5291
5576
  }
5292
5577
  function removeTemplates(log, projectDir, rm) {
5293
- rm(join14(projectDir, log.templates.claudeDir));
5294
- if (log.templates.codexDir) rm(join14(projectDir, log.templates.codexDir));
5295
- if (log.templates.opencodeDir) rm(join14(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));
5296
5581
  const external = removeExternalFiles(log, projectDir, rm);
5297
5582
  const importStripped = stripRootImport(projectDir);
5298
5583
  const rootMd = log.templates.rootClaudeMd;
5299
5584
  if (rootMd) {
5300
5585
  if (rootClaudeMdModified(log, projectDir))
5301
5586
  return { rootClaudeMdKept: true, importStripped, external };
5302
- rm(join14(projectDir, rootMd.path));
5587
+ rm(join16(projectDir, rootMd.path));
5303
5588
  }
5304
5589
  return { rootClaudeMdKept: false, importStripped, external };
5305
5590
  }
@@ -5307,17 +5592,17 @@ function removeExternalFiles(log, projectDir, rm) {
5307
5592
  const removed = [];
5308
5593
  const kept = [];
5309
5594
  for (const { path, sha256 } of log.externalFiles ?? []) {
5310
- const abs = join14(projectDir, path);
5311
- if (!existsSync15(abs)) continue;
5595
+ const abs = join16(projectDir, path);
5596
+ if (!existsSync17(abs)) continue;
5312
5597
  if (!lstatSync2(abs).isFile()) continue;
5313
- if (hashContent(readFileSync14(abs, "utf8")) !== sha256) {
5598
+ if (hashContent(readFileSync15(abs, "utf8")) !== sha256) {
5314
5599
  kept.push(path);
5315
5600
  continue;
5316
5601
  }
5317
5602
  rm(abs);
5318
5603
  removed.push(path);
5319
5604
  }
5320
- for (const path of removed) pruneEmptyDirsUpward(projectDir, dirname5(join14(projectDir, path)));
5605
+ for (const path of removed) pruneEmptyDirsUpward(projectDir, dirname5(join16(projectDir, path)));
5321
5606
  return { removed, kept };
5322
5607
  }
5323
5608
  function pruneEmptyDirsUpward(projectDir, startDir) {
@@ -5326,8 +5611,8 @@ function pruneEmptyDirsUpward(projectDir, startDir) {
5326
5611
  const rel = relative2(projectDir, cursor);
5327
5612
  if (rel === "" || rel.startsWith(`..${sep2}`) || rel === "..") return;
5328
5613
  try {
5329
- if (readdirSync5(cursor).length > 0) return;
5330
- rmdirSync(cursor);
5614
+ if (readdirSync6(cursor).length > 0) return;
5615
+ rmdirSync2(cursor);
5331
5616
  } catch {
5332
5617
  return;
5333
5618
  }
@@ -5339,7 +5624,7 @@ function stripRootImport(projectDir) {
5339
5624
  const stripped = current === null ? null : stripHarnessImport(current);
5340
5625
  if (stripped === null) return false;
5341
5626
  try {
5342
- writeFileSync7(join14(projectDir, "CLAUDE.md"), stripped, "utf8");
5627
+ writeFileSync7(join16(projectDir, "CLAUDE.md"), stripped, "utf8");
5343
5628
  return true;
5344
5629
  } catch {
5345
5630
  return false;
@@ -5349,9 +5634,9 @@ function previewExternalLines(installLog, projectDir) {
5349
5634
  const lines = [];
5350
5635
  let removable = 0;
5351
5636
  for (const { path, sha256 } of installLog.externalFiles ?? []) {
5352
- const abs = join14(projectDir, path);
5353
- if (!existsSync15(abs)) continue;
5354
- if (hashContent(readFileSync14(abs, "utf8")) !== sha256) {
5637
+ const abs = join16(projectDir, path);
5638
+ if (!existsSync17(abs)) continue;
5639
+ if (hashContent(readFileSync15(abs, "utf8")) !== sha256) {
5355
5640
  lines.push(` \u25CB keep ${path} (modified since install \u2014 preserved)`);
5356
5641
  continue;
5357
5642
  }
@@ -5377,10 +5662,10 @@ function hasRootImport(projectDir) {
5377
5662
  return current !== null && stripHarnessImport(current) !== null;
5378
5663
  }
5379
5664
  function readRootClaudeMd(projectDir) {
5380
- const path = join14(projectDir, "CLAUDE.md");
5381
- if (!existsSync15(path)) return null;
5665
+ const path = join16(projectDir, "CLAUDE.md");
5666
+ if (!existsSync17(path)) return null;
5382
5667
  try {
5383
- return readFileSync14(path, "utf8");
5668
+ return readFileSync15(path, "utf8");
5384
5669
  } catch {
5385
5670
  return null;
5386
5671
  }
@@ -5388,9 +5673,9 @@ function readRootClaudeMd(projectDir) {
5388
5673
  function rootClaudeMdModified(log, projectDir) {
5389
5674
  const rootMd = log.templates.rootClaudeMd;
5390
5675
  if (!rootMd) return false;
5391
- const path = join14(projectDir, rootMd.path);
5392
- if (!existsSync15(path)) return false;
5393
- 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;
5394
5679
  }
5395
5680
  function formatTemplateList(log) {
5396
5681
  const items = [log.templates.claudeDir];
@@ -5402,7 +5687,7 @@ function defaultSpawn2(cmd, args) {
5402
5687
  return spawnSync2(cmd, [...args], { encoding: "utf8", stdio: "pipe", timeout: 12e4 });
5403
5688
  }
5404
5689
  function defaultRm(path) {
5405
- if (existsSync15(path)) {
5690
+ if (existsSync17(path)) {
5406
5691
  rmSync(path, { recursive: true, force: true });
5407
5692
  }
5408
5693
  }
@@ -5450,8 +5735,8 @@ import { resolve as resolve5 } from "path";
5450
5735
 
5451
5736
  // src/state.ts
5452
5737
  init_esm_shims();
5453
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
5454
- import { join as join15 } from "path";
5738
+ import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
5739
+ import { join as join17 } from "path";
5455
5740
  var META_FILE = ".claude/.installed-tracks";
5456
5741
  var LEGACY_SIGNATURES = [
5457
5742
  { rule: "htmx.md", track: "ssr-htmx" },
@@ -5461,11 +5746,11 @@ var LEGACY_SIGNATURES = [
5461
5746
  { rule: "cli-development.md", track: "tooling" }
5462
5747
  ];
5463
5748
  function detectInstallState(projectDir) {
5464
- const claudeDir = join15(projectDir, ".claude");
5465
- const hasClaudeDir = existsSync16(claudeDir);
5749
+ const claudeDir = join17(projectDir, ".claude");
5750
+ const hasClaudeDir = existsSync18(claudeDir);
5466
5751
  if (hasClaudeDir) {
5467
- const metaPath = join15(projectDir, META_FILE);
5468
- if (existsSync16(metaPath)) {
5752
+ const metaPath = join17(projectDir, META_FILE);
5753
+ if (existsSync18(metaPath)) {
5469
5754
  const tracks2 = readMetafile(metaPath);
5470
5755
  return { state: "existing", tracks: tracks2, source: "metafile", hasClaudeDir: true };
5471
5756
  }
@@ -5487,7 +5772,7 @@ function tracksFromLog(tracks) {
5487
5772
  return [...new Set(tracks.filter(isTrack))].sort();
5488
5773
  }
5489
5774
  function readMetafile(path) {
5490
- const raw = readFileSync15(path, "utf8");
5775
+ const raw = readFileSync16(path, "utf8");
5491
5776
  const seen = /* @__PURE__ */ new Set();
5492
5777
  for (const line of raw.split(/\s+/)) {
5493
5778
  const trimmed = line.trim();
@@ -5498,13 +5783,13 @@ function readMetafile(path) {
5498
5783
  return [...seen].sort();
5499
5784
  }
5500
5785
  function inferFromLegacySignatures(projectDir) {
5501
- const rulesDir = join15(projectDir, ".claude/rules");
5502
- if (!existsSync16(rulesDir)) {
5786
+ const rulesDir = join17(projectDir, ".claude/rules");
5787
+ if (!existsSync18(rulesDir)) {
5503
5788
  return [];
5504
5789
  }
5505
5790
  const found = /* @__PURE__ */ new Set();
5506
5791
  for (const sig of LEGACY_SIGNATURES) {
5507
- if (existsSync16(join15(rulesDir, sig.rule))) {
5792
+ if (existsSync18(join17(rulesDir, sig.rule))) {
5508
5793
  found.add(sig.track);
5509
5794
  }
5510
5795
  }
@@ -5526,8 +5811,21 @@ function updateAction(options = {}, deps = {}) {
5526
5811
  exit(1);
5527
5812
  return;
5528
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
+ }
5529
5822
  log(c.dim(`Updating installed harness files in ${projectDir}`));
5530
- 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
+ });
5531
5829
  }
5532
5830
  function registerUpdateCommand(cli2) {
5533
5831
  cli2.command(
@@ -5535,7 +5833,10 @@ function registerUpdateCommand(cli2) {
5535
5833
  "Refresh installed policy files (rules / agents / commands / hooks / skills)"
5536
5834
  ).option("--project-dir <path>", "[Project] Target project directory", {
5537
5835
  default: process.cwd()
5538
- }).action((options) => {
5836
+ }).option(
5837
+ "--only <group>",
5838
+ `[Scope] Update only this group (repeatable): ${UPDATE_GROUPS.join(" | ")}`
5839
+ ).action((options) => {
5539
5840
  updateAction(options);
5540
5841
  });
5541
5842
  }
@@ -5650,19 +5951,6 @@ var INSTALL_TARGET_PAGES = [
5650
5951
  { label: "Visual & Media (Slides \xB7 Diagrams \xB7 Motion \xB7 Video)", cats: ["visual-media"] },
5651
5952
  { label: "Workflow & ECC Suite", cats: ["workflow", "ecc-suite"] }
5652
5953
  ];
5653
- var DEV_METHOD_BUNDLE_VALUE = "bundle:dev-method";
5654
- var DEV_METHOD_BUNDLE_CATEGORY = "workflow";
5655
- var bundleMemberValues = () => DEV_METHOD_SKILL_IDS.map((id) => `asset:${id}`);
5656
- function collapseDevMethodBundle(ids) {
5657
- const members = new Set(bundleMemberValues());
5658
- const rest = ids.filter((v2) => !members.has(v2));
5659
- const anyMember = ids.some((v2) => members.has(v2));
5660
- return anyMember ? [...rest, DEV_METHOD_BUNDLE_VALUE] : rest;
5661
- }
5662
- function expandDevMethodBundle(ids) {
5663
- const rest = ids.filter((v2) => v2 !== DEV_METHOD_BUNDLE_VALUE);
5664
- return ids.includes(DEV_METHOD_BUNDLE_VALUE) ? [...rest, ...bundleMemberValues()] : rest;
5665
- }
5666
5954
  var BASELINE_TITLES = {
5667
5955
  rules: "Rules \u2014 \uC0C1\uC2DC \uC0C1\uC8FC\uD558\uB294 \uB300\uC6D0\uCE59 (4 CLI \uC804\uBD80\uC5D0 \uC124\uCE58)",
5668
5956
  hooks: "Hooks \u2014 \uACB0\uC815\uB860\uC801 \uAC00\uB4DC (\uD574\uC81C\uD558\uBA74 settings.json \uBC30\uC120\uB3C4 \uD568\uAED8 \uBE60\uC9C4\uB2E4)",
@@ -5676,7 +5964,8 @@ function buildPageGroups(cats, initialSet, installedSet = /* @__PURE__ */ new Se
5676
5964
  for (const kind of baselineKinds) {
5677
5965
  const items = baselineTargets.filter((t) => t.kind === kind).map((t) => ({
5678
5966
  value: t.id,
5679
- label: ` ${t.name}${installedMark(t.id)}`
5967
+ label: ` ${t.name}${installedMark(t.id)}`,
5968
+ ...t.hint ? { hint: t.hint } : {}
5680
5969
  }));
5681
5970
  if (items.length === 0) continue;
5682
5971
  groups[BASELINE_TITLES[kind]] = items;
@@ -5692,16 +5981,10 @@ function buildPageGroups(cats, initialSet, installedSet = /* @__PURE__ */ new Se
5692
5981
  hint: o.hint
5693
5982
  });
5694
5983
  }
5695
- if (cat === DEV_METHOD_BUNDLE_CATEGORY) {
5696
- items.push({
5697
- value: DEV_METHOD_BUNDLE_VALUE,
5698
- label: ` uzys \uD558\uB124\uC2A4 \uBC29\uBC95\uB860 ${DEV_METHOD_SKILL_IDS.length}\uC885 [uzys] \u2605 official${installedMark(DEV_METHOD_BUNDLE_VALUE)}`,
5699
- hint: DEV_METHOD_SKILL_IDS.join(", ")
5700
- });
5701
- }
5702
5984
  const tierOrder = { official: 0, vetted: 1, experimental: 2 };
5703
- const devMethod = new Set(DEV_METHOD_SKILL_IDS);
5704
- 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
+ );
5705
5988
  for (const a of catAssets) {
5706
5989
  const tier = assetTrustTier(a.id);
5707
5990
  const badge = tier === "official" ? " \u2605 official" : tier === "experimental" ? " \u26A0 experimental (opt-in)" : "";
@@ -5807,6 +6090,42 @@ var defaultPrompts = {
5807
6090
  });
5808
6091
  return q(result) ? null : result;
5809
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
+ },
5810
6129
  confirmInstall: async (summary) => {
5811
6130
  const result = await ue({
5812
6131
  message: `${summary}
@@ -5818,14 +6137,11 @@ Proceed?`,
5818
6137
  },
5819
6138
  selectInstallTargets: async (initialChecked, step, recap) => {
5820
6139
  const pages = INSTALL_TARGET_PAGES;
5821
- const baselineTargets = listBaselineTargets({ tracks: recap?.tracks ?? [] });
5822
- const displayInitial = collapseDevMethodBundle(initialChecked);
5823
- const initialSet = new Set(displayInitial);
5824
- 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);
5825
6143
  const recapLine = recap ? `Tracks: ${recap.tracks.join(", ")} \xB7 CLIs: ${recap.cli.join(", ")}` : "";
5826
- const installedSet = new Set(
5827
- collapseDevMethodBundle(recap?.installed ?? [])
5828
- );
6144
+ const installedSet = new Set(recap?.installed ?? []);
5829
6145
  process.stdout.write("\x1B[?1049h");
5830
6146
  let resultIds = null;
5831
6147
  let aborted = false;
@@ -5885,7 +6201,7 @@ Proceed?`,
5885
6201
  pageIdx++;
5886
6202
  }
5887
6203
  if (!aborted) {
5888
- resultIds = expandDevMethodBundle([...collected]);
6204
+ resultIds = [...collected];
5889
6205
  }
5890
6206
  } finally {
5891
6207
  process.stdout.write("\x1B[?1049l");
@@ -5975,11 +6291,15 @@ async function runInteractive(projectDir, deps = {}) {
5975
6291
  }
5976
6292
  if (action === "update") {
5977
6293
  mode = "update";
5978
- const spec = buildUpdateSpec(projectDir, state.tracks);
5979
- const confirmed = await prompts.confirmInstall(
5980
- `UPDATE policy files only:
5981
- ${formatSummary(spec)}`
5982
- );
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)}`);
5983
6303
  if (!confirmed) {
5984
6304
  prompts.outro("Cancelled.");
5985
6305
  return { ok: false, reason: "cancelled" };
@@ -6126,7 +6446,7 @@ function formatSummary(spec) {
6126
6446
  const cost = formatResidentCostLine(
6127
6447
  residentCost(
6128
6448
  buildManifest(assetSpec).filter(
6129
- (e2) => e2.applies(assetSpec) && !isBaselineExcluded(e2.target, baselineExcluded)
6449
+ (e2) => e2.applies(assetSpec) && !isBaselineExcluded(e2.target, baselineExcluded) && landsOnDisk(e2.target, spec.cli)
6130
6450
  )
6131
6451
  ),
6132
6452
  summarizeContextCost(finalAssets).unmeasuredCount