@uzysjung/agent-harness 26.146.0 → 26.147.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 (34) hide show
  1. package/dist/{chunk-OEIGA5IL.js → chunk-GPH5AWLX.js} +450 -53
  2. package/dist/chunk-GPH5AWLX.js.map +1 -0
  3. package/dist/index.js +607 -530
  4. package/dist/index.js.map +1 -1
  5. package/dist/trust-tier-drift.js +1 -1
  6. package/package.json +4 -2
  7. package/templates/CLAUDE.md +14 -5
  8. package/templates/agents/implementer.md +1 -1
  9. package/templates/codex/AGENTS.md.template +11 -0
  10. package/templates/opencode/AGENTS.md.template +8 -1
  11. package/templates/opencode/opencode.json.template +0 -1
  12. package/templates/rules/cli-development.md +2 -2
  13. package/templates/rules/doc-governance.md +3 -1
  14. package/templates/rules/test-policy.md +5 -6
  15. package/templates/scripts/check-absence.sh +205 -0
  16. package/templates/settings.json +0 -9
  17. package/templates/skills/gh-issue-workflow/ISSUE.template.md +111 -35
  18. package/templates/skills/gh-issue-workflow/SKILL.md +268 -138
  19. package/templates/skills/gh-issue-workflow/references/hierarchy.md +261 -0
  20. package/templates/skills/gh-issue-workflow/references/operating-model.md +132 -0
  21. package/templates/skills/gh-issue-workflow/references/readiness.md +121 -0
  22. package/templates/skills/ui-visual-review/SKILL.md +17 -3
  23. package/dist/chunk-OEIGA5IL.js.map +0 -1
  24. package/templates/commands/ecc/checkpoint.md +0 -32
  25. package/templates/commands/ecc/e2e.md +0 -105
  26. package/templates/commands/ecc/eval.md +0 -88
  27. package/templates/commands/ecc/evolve.md +0 -7
  28. package/templates/commands/ecc/harness-audit.md +0 -73
  29. package/templates/commands/ecc/instinct-status.md +0 -8
  30. package/templates/commands/ecc/promote.md +0 -10
  31. package/templates/commands/ecc/security-scan.md +0 -10
  32. package/templates/hooks/mcp-pre-exec.sh +0 -122
  33. package/templates/mcp-allowlist.example +0 -24
  34. package/templates/rules/playwright-launch.md +0 -16
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ ALL_RULES,
3
4
  CATEGORIES,
4
5
  CATEGORY_TITLES,
5
6
  CLI_BASES,
@@ -8,7 +9,9 @@ import {
8
9
  EXTERNAL_ASSETS,
9
10
  HARNESS_ANCHOR_FILE,
10
11
  HARNESS_IMPORT_LINE,
12
+ INSTALL_LOG_DIR,
11
13
  INTERNAL_BUNDLED_SKILL_IDS,
14
+ POLICY_DIRS,
12
15
  TRACKS,
13
16
  __commonJS,
14
17
  __toESM,
@@ -16,23 +19,44 @@ import {
16
19
  assetCliSupport,
17
20
  assetReachesCli,
18
21
  assetTrustTier,
22
+ backupDir,
23
+ backupFile,
24
+ backupFileIfChanged,
25
+ buildInstallLog,
19
26
  buildManifest,
27
+ collectPolicyHashes,
28
+ collectSkillHashes,
29
+ copyBackupDir,
30
+ copyDir,
31
+ copyFile,
32
+ ensureDir,
33
+ ensureProjectSkeleton,
20
34
  experimentalOptInCandidates,
21
35
  filterApplicableAssets,
22
36
  formatResidentCostLine,
23
37
  hasUiTrack,
38
+ hashContent,
24
39
  init_esm_shims,
40
+ installLogPath,
25
41
  isAssetSelected,
26
42
  isCliBase,
43
+ isCliNeutralTarget,
44
+ isHarnessOwned,
27
45
  isInstallScope,
28
46
  isTrack,
47
+ legacyInstallLogPath,
48
+ listFilesRecursive,
49
+ mergeExternalFiles,
50
+ readInstallLog,
29
51
  renderFillScaffold,
30
52
  residentCost,
53
+ resolveRules,
31
54
  resolveScope,
32
55
  stripHarnessImport,
33
56
  summarizeContextCost,
34
- upsertHarnessImport
35
- } from "./chunk-OEIGA5IL.js";
57
+ upsertHarnessImport,
58
+ writeInstallLog
59
+ } from "./chunk-GPH5AWLX.js";
36
60
 
37
61
  // node_modules/sisteransi/src/index.js
38
62
  var require_src = __commonJS({
@@ -717,7 +741,7 @@ var cac = (name = "") => new CAC(name);
717
741
  // package.json
718
742
  var package_default = {
719
743
  name: "@uzysjung/agent-harness",
720
- version: "26.146.0",
744
+ version: "26.147.0",
721
745
  description: "Curate vetted AI-coding skills & plugins by your tech stack \u2014 install only what you need, across Claude Code, Codex, OpenCode & Antigravity",
722
746
  type: "module",
723
747
  publishConfig: {
@@ -750,7 +774,9 @@ var package_default = {
750
774
  ci: "npm run typecheck && npm run lint && npm run test:coverage && npm run build",
751
775
  prepare: "[ -d dist ] || npm run build",
752
776
  "cost:report": "npm run build && node scripts/context-cost-report.mjs",
753
- "cost:baseline": "npm run build && node scripts/context-cost-baseline.mjs"
777
+ "cost:baseline": "npm run build && node scripts/context-cost-baseline.mjs",
778
+ security: "npm audit --omit=dev --audit-level=high && npx --yes ecc-agentshield@1.4.0 scan --baseline security-baseline.json --gate",
779
+ "security:baseline": "npx --yes ecc-agentshield@1.4.0 scan --save-baseline security-baseline.json"
754
780
  },
755
781
  dependencies: {
756
782
  "@clack/prompts": "^1.3.0",
@@ -776,6 +802,46 @@ init_esm_shims();
776
802
  import { resolve as resolve2 } from "path";
777
803
  import { fileURLToPath } from "url";
778
804
 
805
+ // src/baseline-targets.ts
806
+ init_esm_shims();
807
+ var BASELINE_KINDS = ["rules", "agents", "hooks", "skills"];
808
+ var BASELINE_PREFIX = "baseline:";
809
+ var PREFIX_BY_KIND = [
810
+ ["rules", ".claude/rules/"],
811
+ ["agents", ".claude/agents/"],
812
+ ["hooks", ".claude/hooks/"],
813
+ ["skills", ".claude/skills/"]
814
+ ];
815
+ function classifyBaselineTarget(target) {
816
+ for (const [kind, prefix] of PREFIX_BY_KIND) {
817
+ if (!target.startsWith(prefix)) continue;
818
+ const rest = target.slice(prefix.length);
819
+ if (rest === "") return null;
820
+ const first = rest.split("/")[0] ?? "";
821
+ const name = kind === "skills" ? first : first.replace(/\.(md|sh)$/, "");
822
+ if (name === "") return null;
823
+ return { id: `${BASELINE_PREFIX}${kind}/${name}`, kind, name };
824
+ }
825
+ return null;
826
+ }
827
+ function listBaselineTargets(spec) {
828
+ const byId = /* @__PURE__ */ new Map();
829
+ for (const entry of buildManifest(spec)) {
830
+ if (!entry.applies(spec)) continue;
831
+ const t = classifyBaselineTarget(entry.target);
832
+ if (t && !byId.has(t.id)) byId.set(t.id, t);
833
+ }
834
+ const order = new Map(BASELINE_KINDS.map((k2, i) => [k2, i]));
835
+ return [...byId.values()].sort(
836
+ (a, b2) => (order.get(a.kind) ?? 0) - (order.get(b2.kind) ?? 0) || a.name.localeCompare(b2.name)
837
+ );
838
+ }
839
+ function isBaselineExcluded(target, excluded) {
840
+ if (excluded.size === 0) return false;
841
+ const t = classifyBaselineTarget(target);
842
+ return t !== null && excluded.has(t.id);
843
+ }
844
+
779
845
  // src/cli-targets.ts
780
846
  init_esm_shims();
781
847
  var CLI_BASE_SORT_ORDER = {
@@ -913,127 +979,18 @@ init_esm_shims();
913
979
  import {
914
980
  chmodSync as chmodSync2,
915
981
  existsSync as existsSync13,
916
- mkdirSync as mkdirSync6,
917
- readdirSync as readdirSync4,
982
+ mkdirSync as mkdirSync4,
983
+ readdirSync as readdirSync3,
918
984
  readFileSync as readFileSync12,
919
- writeFileSync as writeFileSync7
985
+ unlinkSync as unlinkSync2,
986
+ writeFileSync as writeFileSync6
920
987
  } from "fs";
921
- import { basename as basename5, dirname as dirname6, join as join11, resolve } from "path";
988
+ import { basename as basename5, dirname as dirname4, join as join11, resolve } from "path";
922
989
 
923
990
  // src/ci-scaffold.ts
924
991
  init_esm_shims();
925
- import { existsSync as existsSync2 } from "fs";
926
- import { join as join2 } from "path";
927
-
928
- // src/fs-ops.ts
929
- init_esm_shims();
930
- import {
931
- copyFileSync,
932
- cpSync,
933
- existsSync,
934
- constants as fsConstants,
935
- mkdirSync,
936
- readdirSync,
937
- readFileSync,
938
- realpathSync,
939
- renameSync,
940
- rmdirSync
941
- } from "fs";
942
- import { dirname, join } from "path";
943
- function ensureDir(path) {
944
- mkdirSync(path, { recursive: true });
945
- }
946
- function copyFile(source, target) {
947
- if (!existsSync(source)) {
948
- throw new Error(`Source not found: ${source}`);
949
- }
950
- mkdirSync(dirname(target), { recursive: true });
951
- copyFileSync(source, target);
952
- }
953
- function copyDir(source, target) {
954
- if (!existsSync(source)) {
955
- throw new Error(`Source dir not found: ${source}`);
956
- }
957
- mkdirSync(target, { recursive: true });
958
- cpSync(source, target, { recursive: true, force: true });
959
- }
960
- var MAX_BACKUP_CLAIMS = 100;
961
- function claimBackupPath(base, claim) {
962
- for (let n = 1; n <= MAX_BACKUP_CLAIMS; n++) {
963
- const candidate = n === 1 ? base : `${base}-${n}`;
964
- try {
965
- claim(candidate);
966
- return candidate;
967
- } catch (err) {
968
- if (err.code !== "EEXIST") throw err;
969
- }
970
- }
971
- throw new Error(`Cannot allocate a backup path for ${base} (${MAX_BACKUP_CLAIMS} taken)`);
972
- }
973
- function backupDir(target, now = /* @__PURE__ */ new Date()) {
974
- if (!existsSync(target)) {
975
- return null;
976
- }
977
- const backup = claimBackupPath(`${target}.backup-${formatStamp(now)}`, (c3) => mkdirSync(c3));
978
- rmdirSync(backup);
979
- renameSync(target, backup);
980
- return backup;
981
- }
982
- function copyBackupDir(target, now = /* @__PURE__ */ new Date()) {
983
- if (!existsSync(target)) {
984
- return null;
985
- }
986
- const backup = claimBackupPath(`${target}.backup-${formatStamp(now)}`, (c3) => mkdirSync(c3));
987
- cpSync(realpathSync(target), backup, { recursive: true });
988
- return backup;
989
- }
990
- function backupFileIfChanged(target, newContent, now = /* @__PURE__ */ new Date()) {
991
- if (!existsSync(target)) {
992
- return null;
993
- }
994
- if (readFileSync(target, "utf-8") === newContent) {
995
- return null;
996
- }
997
- return backupFile(target, now);
998
- }
999
- function backupFile(target, now = /* @__PURE__ */ new Date()) {
1000
- return claimBackupPath(
1001
- `${target}.backup-${formatStamp(now)}`,
1002
- (c3) => copyFileSync(target, c3, fsConstants.COPYFILE_EXCL)
1003
- );
1004
- }
1005
- function listFilesRecursive(dir, prefix = "") {
1006
- if (!existsSync(dir)) return [];
1007
- const out = [];
1008
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1009
- const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
1010
- if (entry.isDirectory()) {
1011
- out.push(...listFilesRecursive(join(dir, entry.name), rel));
1012
- } else if (entry.isFile()) {
1013
- out.push(rel);
1014
- }
1015
- }
1016
- return out;
1017
- }
1018
- function formatStamp(now) {
1019
- return now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z").slice(0, 15);
1020
- }
1021
- function ensureProjectSkeleton(projectDir) {
1022
- const dirs = [
1023
- ".claude/commands/uzys",
1024
- ".claude/commands/ecc",
1025
- ".claude/rules",
1026
- ".claude/skills",
1027
- ".claude/agents",
1028
- ".claude/hooks",
1029
- "docs/decisions"
1030
- ];
1031
- for (const d2 of dirs) {
1032
- mkdirSync(join(projectDir, d2), { recursive: true });
1033
- }
1034
- }
1035
-
1036
- // src/ci-scaffold.ts
992
+ import { existsSync } from "fs";
993
+ import { join } from "path";
1037
994
  var PYTHON_TRACKS = "data|csr-fastapi|full";
1038
995
  var VARIANTS = [
1039
996
  {
@@ -1057,12 +1014,12 @@ function installCiScaffold(ctx) {
1057
1014
  for (const v2 of VARIANTS) {
1058
1015
  if (!v2.applies(ctx.tracks)) continue;
1059
1016
  const relTarget = `.github/workflows/${v2.target}`;
1060
- const target = join2(ctx.projectDir, relTarget);
1061
- if (existsSync2(target)) {
1017
+ const target = join(ctx.projectDir, relTarget);
1018
+ if (existsSync(target)) {
1062
1019
  report.skippedExisting.push(relTarget);
1063
1020
  continue;
1064
1021
  }
1065
- copyFile(join2(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
1022
+ copyFile(join(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
1066
1023
  report.written.push(relTarget);
1067
1024
  }
1068
1025
  return report;
@@ -1073,8 +1030,8 @@ init_esm_shims();
1073
1030
 
1074
1031
  // src/antigravity/transform.ts
1075
1032
  init_esm_shims();
1076
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1077
- import { basename, join as join4 } from "path";
1033
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1034
+ import { basename, join as join3 } from "path";
1078
1035
 
1079
1036
  // src/codex/agents-md.ts
1080
1037
  init_esm_shims();
@@ -1083,7 +1040,7 @@ function renameSlashes(text) {
1083
1040
  }
1084
1041
  function renderAgentsMd(params) {
1085
1042
  const body = params.claudeMd.replace(/^#\s+.*\r?\n/, "").trim();
1086
- const replaced = params.template.replaceAll("{PROJECT_NAME}", params.projectName).replaceAll("{PROJECT_RULES}", body).replaceAll("{PROJECT_CONTEXT}", params.projectContext);
1043
+ const replaced = params.template.replaceAll("{PROJECT_NAME}", params.projectName).replaceAll("{PROJECT_RULES}", body).replaceAll("{PROJECT_CONTEXT}", params.projectContext).replaceAll("{HARNESS_RULES}", params.harnessRules ?? "");
1087
1044
  return renameSlashes(replaced);
1088
1045
  }
1089
1046
 
@@ -1119,204 +1076,8 @@ function portBody(body) {
1119
1076
 
1120
1077
  // src/owned-write.ts
1121
1078
  init_esm_shims();
1122
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1123
- import { dirname as dirname3, relative, sep } from "path";
1124
-
1125
- // src/install-log.ts
1126
- init_esm_shims();
1127
- import { createHash } from "crypto";
1128
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
1129
- import { dirname as dirname2, join as join3 } from "path";
1130
- var INSTALL_LOG_FILENAME = ".harness-install.json";
1131
- var INSTALL_LOG_DIR = ".uzys-agent-harness";
1132
- var LEGACY_INSTALL_LOG_DIR = ".claude";
1133
- var INSTALL_LOG_VERSION = 1;
1134
- var POLICY_DIRS = [
1135
- { dir: "rules", ext: ".md" },
1136
- { dir: "agents", ext: ".md" },
1137
- { dir: "commands/uzys", ext: ".md" },
1138
- { dir: "hooks", ext: ".sh" }
1139
- ];
1140
- function buildAssetEntries(report, scope) {
1141
- if (!report) return [];
1142
- return report.attempted.filter((r) => r.ok).map((r) => assetToLogEntry(r.asset, scope, r.version));
1143
- }
1144
- function assetToLogEntry(asset, scope, version) {
1145
- const detail = methodDetail(asset.method);
1146
- const entry = {
1147
- id: asset.id,
1148
- category: asset.category,
1149
- method: asset.method.kind,
1150
- scope,
1151
- detail
1152
- };
1153
- if (version) entry.version = version;
1154
- return entry;
1155
- }
1156
- function methodDetail(method) {
1157
- switch (method.kind) {
1158
- case "plugin":
1159
- return { marketplace: method.marketplace, pluginId: method.pluginId };
1160
- case "skill":
1161
- return { source: method.source, ...method.skill ? { skill: method.skill } : {} };
1162
- case "npm":
1163
- return { pkg: method.pkg };
1164
- case "npx-run":
1165
- return { cmd: method.cmd, args: (method.args ?? []).join(" ") };
1166
- case "shell-script":
1167
- return { script: method.script, args: method.args.join(" ") };
1168
- case "internal":
1169
- return { key: method.key };
1170
- }
1171
- }
1172
- function buildInstallLog(spec, external, scope, rootClaudeMd, previous, claudeDirMovedAside = false, rootFiles = []) {
1173
- const templates = {
1174
- claudeDir: ".claude/",
1175
- ...spec.cli.includes("codex") ? { codexDir: ".codex/" } : {},
1176
- ...spec.cli.includes("opencode") ? { opencodeDir: ".opencode/" } : {},
1177
- ...rootClaudeMd ? { rootClaudeMd } : {}
1178
- };
1179
- const log = {
1180
- schemaVersion: INSTALL_LOG_VERSION,
1181
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1182
- scope,
1183
- spec: {
1184
- tracks: spec.tracks,
1185
- cli: spec.cli
1186
- },
1187
- // 이번 설치가 만든 항목이 이기고, 이번에 안 만든 항목은 이전 값을 그대로 둔다.
1188
- // (예: claude 로 깔고 나중에 codex 만 추가 설치해도 root CLAUDE.md 기록이 살아남는다)
1189
- templates: { ...previous?.templates, ...templates },
1190
- assets: mergeAssets(
1191
- claudeDirMovedAside ? previous?.assets?.filter(survivesClaudeDirRename) : previous?.assets,
1192
- buildAssetEntries(external, scope)
1193
- )
1194
- };
1195
- const mergedRootFiles = mergeRootFiles(previous?.rootFiles, rootFiles);
1196
- if (mergedRootFiles.length > 0) log.rootFiles = mergedRootFiles;
1197
- return log;
1198
- }
1199
- function mergeRootFiles(previous, current) {
1200
- const byPath = /* @__PURE__ */ new Map();
1201
- for (const file of [...previous ?? [], ...current]) {
1202
- const prior = byPath.get(file.path);
1203
- byPath.set(
1204
- file.path,
1205
- prior ? {
1206
- path: file.path,
1207
- change: prior.change === "created" ? "created" : file.change,
1208
- notes: [.../* @__PURE__ */ new Set([...prior.notes, ...file.notes])]
1209
- } : file
1210
- );
1211
- }
1212
- return [...byPath.values()];
1213
- }
1214
- function survivesClaudeDirRename(asset) {
1215
- if (asset.scope === "global") return true;
1216
- switch (asset.method) {
1217
- case "plugin":
1218
- return true;
1219
- // `~/.claude/plugins/cache` — 프로젝트 밖
1220
- case "npm":
1221
- return true;
1222
- // `node_modules/`
1223
- case "skill":
1224
- return false;
1225
- // `npx skills add` project scope → `.claude/skills/`
1226
- case "shell-script":
1227
- return false;
1228
- // ecc-prune → `.claude/local-plugins/`
1229
- case "npx-run":
1230
- return false;
1231
- case "internal":
1232
- return false;
1233
- }
1234
- }
1235
- function mergeAssets(previous, current) {
1236
- if (!previous || previous.length === 0) return [...current];
1237
- const currentById = new Map(current.map((a) => [a.id, a]));
1238
- const previousIds = new Set(previous.map((a) => a.id));
1239
- return [
1240
- ...previous.map((a) => currentById.get(a.id) ?? a),
1241
- ...current.filter((a) => !previousIds.has(a.id))
1242
- ];
1243
- }
1244
- function hashContent(content) {
1245
- return createHash("sha256").update(content, "utf8").digest("hex");
1246
- }
1247
- function isHarnessOwned(baseline, key, current) {
1248
- const recorded = baseline.get(key);
1249
- return recorded !== void 0 && recorded === hashContent(current);
1250
- }
1251
- function mergeExternalFiles(projectDir, previous, current) {
1252
- const byPath = /* @__PURE__ */ new Map();
1253
- for (const file of [...previous ?? [], ...current]) byPath.set(file.path, file);
1254
- return [...byPath.values()].filter((f) => existsSync3(join3(projectDir, f.path)));
1255
- }
1256
- function collectSkillHashes(projectDir) {
1257
- const skillsDir = join3(projectDir, ".claude/skills");
1258
- return listFilesRecursive(skillsDir).map((rel) => ({
1259
- path: rel,
1260
- sha256: hashContent(readFileSync2(join3(skillsDir, rel), "utf8"))
1261
- }));
1262
- }
1263
- function collectPolicyHashes(projectDir, templatesDir) {
1264
- const out = [];
1265
- for (const { dir, ext } of POLICY_DIRS) {
1266
- const targetDir = join3(projectDir, ".claude", dir);
1267
- const sourceDir = join3(templatesDir, dir);
1268
- for (const rel of listFilesRecursive(targetDir)) {
1269
- if (!rel.endsWith(ext)) continue;
1270
- if (!existsSync3(join3(sourceDir, rel))) continue;
1271
- out.push({
1272
- path: `${dir}/${rel}`,
1273
- sha256: hashContent(readFileSync2(join3(targetDir, rel), "utf8"))
1274
- });
1275
- }
1276
- }
1277
- return out;
1278
- }
1279
- function writeInstallLog(projectDir, log) {
1280
- const path = installLogPath(projectDir);
1281
- mkdirSync2(dirname2(path), { recursive: true });
1282
- writeFileSync(path, `${JSON.stringify(log, null, 2)}
1283
- `, "utf8");
1284
- migrateAwayLegacyLog(projectDir);
1285
- return path;
1286
- }
1287
- function migrateAwayLegacyLog(projectDir) {
1288
- const legacy = legacyInstallLogPath(projectDir);
1289
- if (!existsSync3(legacy)) return;
1290
- try {
1291
- rmSync(legacy);
1292
- } catch {
1293
- }
1294
- }
1295
- function readInstallLog(projectDir) {
1296
- const path = [installLogPath(projectDir), legacyInstallLogPath(projectDir)].find(
1297
- (p2) => existsSync3(p2)
1298
- );
1299
- if (!path) return null;
1300
- try {
1301
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
1302
- if (Array.isArray(parsed.assets)) {
1303
- parsed.assets = parsed.assets.map(
1304
- (a) => a.method === "npm-global" ? { ...a, method: "npm" } : a
1305
- );
1306
- }
1307
- return parsed;
1308
- } catch {
1309
- return null;
1310
- }
1311
- }
1312
- function installLogPath(projectDir) {
1313
- return join3(projectDir, INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
1314
- }
1315
- function legacyInstallLogPath(projectDir) {
1316
- return join3(projectDir, LEGACY_INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
1317
- }
1318
-
1319
- // src/owned-write.ts
1079
+ import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
1080
+ import { dirname, relative, sep } from "path";
1320
1081
  function createOwnedWriter(projectDir, baseline, options = {}) {
1321
1082
  const { now = /* @__PURE__ */ new Date(), refreshOnly = false } = options;
1322
1083
  const written = /* @__PURE__ */ new Map();
@@ -1324,22 +1085,22 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
1324
1085
  const backupPaths = [];
1325
1086
  let updated = 0;
1326
1087
  return {
1327
- write(absPath, content) {
1088
+ write(absPath, content, opts) {
1328
1089
  const rel = relative(projectDir, absPath).split(sep).join("/");
1329
1090
  const digest = hashContent(content);
1330
- if (!existsSync4(absPath)) {
1331
- if (refreshOnly) return false;
1332
- mkdirSync3(dirname3(absPath), { recursive: true });
1333
- writeFileSync2(absPath, content);
1091
+ if (!existsSync2(absPath)) {
1092
+ if (refreshOnly && !opts?.createInRefresh) return false;
1093
+ mkdirSync(dirname(absPath), { recursive: true });
1094
+ writeFileSync(absPath, content);
1334
1095
  updated++;
1335
1096
  } else {
1336
- const current = readFileSync3(absPath, "utf8");
1097
+ const current = readFileSync(absPath, "utf8");
1337
1098
  if (current !== content) {
1338
1099
  if (!isHarnessOwned(baseline, rel, current)) {
1339
1100
  backupPaths.push(backupFile(absPath, now));
1340
1101
  backedUp.push(rel);
1341
1102
  }
1342
- writeFileSync2(absPath, content);
1103
+ writeFileSync(absPath, content);
1343
1104
  updated++;
1344
1105
  }
1345
1106
  }
@@ -1357,36 +1118,78 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
1357
1118
  };
1358
1119
  }
1359
1120
 
1121
+ // src/rules-port.ts
1122
+ init_esm_shims();
1123
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1124
+ import { join as join2 } from "path";
1125
+ function stripClaudeFrontmatter(source) {
1126
+ if (!source.startsWith("---\n")) return source.trim();
1127
+ const end = source.indexOf("\n---", 4);
1128
+ if (end === -1) return source.trim();
1129
+ return source.slice(end + 4).trim();
1130
+ }
1131
+ function portRules(harnessRoot, ruleNames) {
1132
+ const ported = [];
1133
+ for (const name of ruleNames) {
1134
+ const src = join2(harnessRoot, "templates/rules", `${name}.md`);
1135
+ if (!existsSync3(src)) continue;
1136
+ ported.push({ name, body: stripClaudeFrontmatter(readFileSync2(src, "utf8")) });
1137
+ }
1138
+ return ported;
1139
+ }
1140
+ function renderRulesBlock(rules) {
1141
+ if (rules.length === 0) return "";
1142
+ return rules.map((r) => r.body.replace(/^#\s+/, "## ")).join("\n\n---\n\n");
1143
+ }
1144
+
1360
1145
  // src/antigravity/transform.ts
1361
1146
  function runAntigravityTransform(params) {
1362
- const { harnessRoot, projectDir, selectedInternalSkills = [], baseline, refreshOnly } = params;
1147
+ const {
1148
+ harnessRoot,
1149
+ projectDir,
1150
+ selectedInternalSkills = [],
1151
+ rules = [],
1152
+ baseline,
1153
+ refreshOnly
1154
+ } = params;
1363
1155
  const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
1364
1156
  const rulesFile = writeRules(harnessRoot, projectDir, writer);
1157
+ const antigravityInstalled = rulesFile !== null;
1158
+ const harnessRuleFiles = [];
1159
+ for (const rule of portRules(harnessRoot, rules)) {
1160
+ const target = join3(projectDir, ".agents", "rules", `${rule.name}.md`);
1161
+ if (!writer.write(target, `${rule.body}
1162
+ `, { createInRefresh: antigravityInstalled })) {
1163
+ continue;
1164
+ }
1165
+ harnessRuleFiles.push(target);
1166
+ }
1365
1167
  const skillFiles = [];
1366
1168
  for (const id of selectedInternalSkills) {
1367
- const src = join4(harnessRoot, "templates/skills", id, "SKILL.md");
1368
- if (!existsSync5(src)) {
1169
+ const src = join3(harnessRoot, "templates/skills", id, "SKILL.md");
1170
+ if (!existsSync4(src)) {
1369
1171
  continue;
1370
1172
  }
1371
- const target = join4(projectDir, ".agents", "skills", id, "SKILL.md");
1372
- if (!writer.write(target, renderBundledSkill(readFileSync4(src, "utf8")))) continue;
1173
+ const target = join3(projectDir, ".agents", "skills", id, "SKILL.md");
1174
+ if (!writer.write(target, renderBundledSkill(readFileSync3(src, "utf8")))) continue;
1373
1175
  skillFiles.push(target);
1374
1176
  }
1375
1177
  return {
1376
1178
  rulesFile,
1179
+ harnessRuleFiles,
1377
1180
  skillFiles,
1378
1181
  ownership: writer.result()
1379
1182
  };
1380
1183
  }
1381
1184
  function writeRules(harnessRoot, projectDir, writer) {
1382
- const claudeMdPath = join4(harnessRoot, "templates/CLAUDE.md");
1383
- const templatePath = join4(harnessRoot, "templates/antigravity/AGENTS.md.template");
1384
- if (!existsSync5(claudeMdPath) || !existsSync5(templatePath)) {
1185
+ const claudeMdPath = join3(harnessRoot, "templates/CLAUDE.md");
1186
+ const templatePath = join3(harnessRoot, "templates/antigravity/AGENTS.md.template");
1187
+ if (!existsSync4(claudeMdPath) || !existsSync4(templatePath)) {
1385
1188
  return null;
1386
1189
  }
1387
- const claudeMd = readFileSync4(claudeMdPath, "utf8");
1388
- const template = readFileSync4(templatePath, "utf8");
1389
- const target = join4(projectDir, ".agents", "rules", "uzys-harness.md");
1190
+ const claudeMd = readFileSync3(claudeMdPath, "utf8");
1191
+ const template = readFileSync3(templatePath, "utf8");
1192
+ const target = join3(projectDir, ".agents", "rules", "uzys-harness.md");
1390
1193
  const rulesOut = renderAgentsMd({
1391
1194
  template,
1392
1195
  claudeMd,
@@ -1399,18 +1202,18 @@ function writeRules(harnessRoot, projectDir, writer) {
1399
1202
  // src/codex/opt-in.ts
1400
1203
  init_esm_shims();
1401
1204
  import { homedir } from "os";
1402
- import { join as join5 } from "path";
1205
+ import { join as join4 } from "path";
1403
1206
 
1404
1207
  // src/codex/trust-entry.ts
1405
1208
  init_esm_shims();
1406
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1407
- import { dirname as dirname4 } from "path";
1209
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1210
+ import { dirname as dirname2 } from "path";
1408
1211
  var TRUST_BLOCK_REGEX = /\[projects\."([^"]+)"\]/g;
1409
1212
  function registerTrustEntry(opts) {
1410
1213
  const { configPath, projectDir } = opts;
1411
1214
  try {
1412
- mkdirSync4(dirname4(configPath), { recursive: true });
1413
- const existing = existsSync6(configPath) ? readFileSync5(configPath, "utf8") : "";
1215
+ mkdirSync2(dirname2(configPath), { recursive: true });
1216
+ const existing = existsSync5(configPath) ? readFileSync4(configPath, "utf8") : "";
1414
1217
  if (hasTrustEntry(existing, projectDir)) {
1415
1218
  return { status: "already-present" };
1416
1219
  }
@@ -1418,7 +1221,7 @@ function registerTrustEntry(opts) {
1418
1221
  [projects."${projectDir}"]
1419
1222
  trust_level = "trusted"
1420
1223
  `;
1421
- writeFileSync3(configPath, existing + block);
1224
+ writeFileSync2(configPath, existing + block);
1422
1225
  return { status: "registered" };
1423
1226
  } catch (e2) {
1424
1227
  return {
@@ -1434,8 +1237,8 @@ function hasTrustEntry(configContent, projectDir) {
1434
1237
 
1435
1238
  // src/codex/opt-in.ts
1436
1239
  function runCodexOptIn(ctx) {
1437
- const codexHome = ctx.codexHome ?? join5(homedir(), ".codex");
1438
- const configPath = join5(codexHome, "config.toml");
1240
+ const codexHome = ctx.codexHome ?? join4(homedir(), ".codex");
1241
+ const configPath = join4(codexHome, "config.toml");
1439
1242
  const result = registerTrustEntry({ configPath, projectDir: ctx.projectDir });
1440
1243
  return {
1441
1244
  trustEntry: {
@@ -1448,8 +1251,8 @@ function runCodexOptIn(ctx) {
1448
1251
 
1449
1252
  // src/codex/transform.ts
1450
1253
  init_esm_shims();
1451
- import { chmodSync, existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1452
- import { basename as basename2, join as join6 } from "path";
1254
+ import { chmodSync, existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
1255
+ import { basename as basename2, join as join5 } from "path";
1453
1256
 
1454
1257
  // src/codex/config-toml.ts
1455
1258
  init_esm_shims();
@@ -1517,23 +1320,32 @@ function quoteIfNeeded(name) {
1517
1320
  var HOOK_NAMES = ["session-start"];
1518
1321
  var ENV_VAR_RENAME = /CLAUDE_PROJECT_DIR/g;
1519
1322
  function runCodexTransform(params) {
1520
- const { harnessRoot, projectDir, selectedInternalSkills = [], baseline, refreshOnly } = params;
1323
+ const {
1324
+ harnessRoot,
1325
+ projectDir,
1326
+ selectedInternalSkills = [],
1327
+ rules = [],
1328
+ baseline,
1329
+ refreshOnly
1330
+ } = params;
1521
1331
  const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
1522
- const claudeMd = readRequired(join6(harnessRoot, "templates/CLAUDE.md"));
1523
- const agentsTemplate = readRequired(join6(harnessRoot, "templates/codex/AGENTS.md.template"));
1524
- const configTemplate = readRequired(join6(harnessRoot, "templates/codex/config.toml.template"));
1332
+ const claudeMd = readRequired(join5(harnessRoot, "templates/CLAUDE.md"));
1333
+ const agentsTemplate = readRequired(join5(harnessRoot, "templates/codex/AGENTS.md.template"));
1334
+ const configTemplate = readRequired(join5(harnessRoot, "templates/codex/config.toml.template"));
1525
1335
  const projectName = basename2(projectDir);
1526
- const mcp = readOptionalJson(join6(harnessRoot, ".mcp.json"));
1527
- const agentsMdPath = join6(projectDir, "AGENTS.md");
1336
+ const mcp = readOptionalJson(join5(harnessRoot, ".mcp.json"));
1337
+ const agentsMdPath = join5(projectDir, "AGENTS.md");
1528
1338
  ensureDir(projectDir);
1529
1339
  const agentsMdOut = renderAgentsMd({
1530
1340
  template: agentsTemplate,
1531
1341
  claudeMd,
1532
1342
  projectName,
1533
- projectContext: renderFillScaffold()
1343
+ projectContext: renderFillScaffold(),
1344
+ // Codex 는 룰 디렉터리가 없다 — 룰이 AGENTS.md 본문에 들어가야 도달한다(§Harness Rules).
1345
+ harnessRules: renderRulesBlock(portRules(harnessRoot, rules))
1534
1346
  });
1535
1347
  writer.write(agentsMdPath, agentsMdOut);
1536
- const configTomlPath = join6(projectDir, ".codex/config.toml");
1348
+ const configTomlPath = join5(projectDir, ".codex/config.toml");
1537
1349
  writer.write(
1538
1350
  configTomlPath,
1539
1351
  renderConfigToml({
@@ -1543,27 +1355,27 @@ function runCodexTransform(params) {
1543
1355
  mcp
1544
1356
  })
1545
1357
  );
1546
- const hookDir = join6(projectDir, ".codex/hooks");
1358
+ const hookDir = join5(projectDir, ".codex/hooks");
1547
1359
  const hookFiles = [];
1548
1360
  for (const hook of HOOK_NAMES) {
1549
- const src = join6(harnessRoot, "templates/hooks", `${hook}.sh`);
1550
- if (!existsSync7(src)) {
1361
+ const src = join5(harnessRoot, "templates/hooks", `${hook}.sh`);
1362
+ if (!existsSync6(src)) {
1551
1363
  continue;
1552
1364
  }
1553
- const ported = readFileSync6(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
1554
- const target = join6(hookDir, `${hook}.sh`);
1365
+ const ported = readFileSync5(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
1366
+ const target = join5(hookDir, `${hook}.sh`);
1555
1367
  if (!writer.write(target, ported)) continue;
1556
1368
  chmodSync(target, 493);
1557
1369
  hookFiles.push(target);
1558
1370
  }
1559
1371
  const skillFiles = [];
1560
1372
  for (const id of selectedInternalSkills) {
1561
- const src = join6(harnessRoot, "templates/skills", id, "SKILL.md");
1562
- if (!existsSync7(src)) {
1373
+ const src = join5(harnessRoot, "templates/skills", id, "SKILL.md");
1374
+ if (!existsSync6(src)) {
1563
1375
  continue;
1564
1376
  }
1565
- const target = join6(projectDir, ".agents", "skills", id, "SKILL.md");
1566
- if (!writer.write(target, renderBundledSkill(readFileSync6(src, "utf8")))) continue;
1377
+ const target = join5(projectDir, ".agents", "skills", id, "SKILL.md");
1378
+ if (!writer.write(target, renderBundledSkill(readFileSync5(src, "utf8")))) continue;
1567
1379
  skillFiles.push(target);
1568
1380
  }
1569
1381
  return {
@@ -1575,17 +1387,17 @@ function runCodexTransform(params) {
1575
1387
  };
1576
1388
  }
1577
1389
  function readRequired(path) {
1578
- if (!existsSync7(path)) {
1390
+ if (!existsSync6(path)) {
1579
1391
  throw new Error(`Codex transform: required source missing: ${path}`);
1580
1392
  }
1581
- return readFileSync6(path, "utf8");
1393
+ return readFileSync5(path, "utf8");
1582
1394
  }
1583
1395
  function readOptionalJson(path) {
1584
- if (!existsSync7(path)) {
1396
+ if (!existsSync6(path)) {
1585
1397
  return null;
1586
1398
  }
1587
1399
  try {
1588
- return JSON.parse(readFileSync6(path, "utf8"));
1400
+ return JSON.parse(readFileSync5(path, "utf8"));
1589
1401
  } catch {
1590
1402
  return null;
1591
1403
  }
@@ -1593,8 +1405,8 @@ function readOptionalJson(path) {
1593
1405
 
1594
1406
  // src/opencode/transform.ts
1595
1407
  init_esm_shims();
1596
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
1597
- import { basename as basename3, join as join7 } from "path";
1408
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1409
+ import { basename as basename3, join as join6 } from "path";
1598
1410
 
1599
1411
  // src/opencode/agents-md.ts
1600
1412
  init_esm_shims();
@@ -1603,7 +1415,7 @@ function renameSlashes2(text) {
1603
1415
  }
1604
1416
  function renderAgentsMd2(params) {
1605
1417
  const body = params.claudeMd.replace(/^#\s+.*\r?\n/, "").trim();
1606
- const replaced = params.template.replaceAll("{PROJECT_NAME}", params.projectName).replaceAll("{PROJECT_RULES}", body).replaceAll("{PROJECT_CONTEXT}", params.projectContext);
1418
+ const replaced = params.template.replaceAll("{PROJECT_NAME}", params.projectName).replaceAll("{PROJECT_RULES}", body).replaceAll("{PROJECT_CONTEXT}", params.projectContext).replaceAll("{HARNESS_RULES}", params.harnessRules ?? "");
1607
1419
  return renameSlashes2(replaced);
1608
1420
  }
1609
1421
 
@@ -1694,39 +1506,50 @@ function parseTemplate(template) {
1694
1506
 
1695
1507
  // src/opencode/transform.ts
1696
1508
  function runOpencodeTransform(params) {
1697
- const { harnessRoot, projectDir, selectedInternalSkills = [], baseline, refreshOnly } = params;
1509
+ const {
1510
+ harnessRoot,
1511
+ projectDir,
1512
+ selectedInternalSkills = [],
1513
+ rules = [],
1514
+ baseline,
1515
+ refreshOnly
1516
+ } = params;
1698
1517
  const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
1699
- const claudeMd = readRequired2(join7(harnessRoot, "templates/CLAUDE.md"));
1700
- const agentsTemplate = readRequired2(join7(harnessRoot, "templates/opencode/AGENTS.md.template"));
1518
+ const claudeMd = readRequired2(join6(harnessRoot, "templates/CLAUDE.md"));
1519
+ const agentsTemplate = readRequired2(join6(harnessRoot, "templates/opencode/AGENTS.md.template"));
1701
1520
  const opencodeTemplate = readRequired2(
1702
- join7(harnessRoot, "templates/opencode/opencode.json.template")
1521
+ join6(harnessRoot, "templates/opencode/opencode.json.template")
1703
1522
  );
1704
1523
  const projectName = basename3(projectDir);
1705
- const mcp = readOptionalJson2(join7(harnessRoot, ".mcp.json"));
1524
+ const mcp = readOptionalJson2(join6(harnessRoot, ".mcp.json"));
1706
1525
  ensureDir(projectDir);
1707
- const agentsMdPath = join7(projectDir, "AGENTS.md");
1526
+ const agentsMdPath = join6(projectDir, "AGENTS.md");
1708
1527
  const agentsMdOut = renderAgentsMd2({
1709
1528
  template: agentsTemplate,
1710
1529
  claudeMd,
1711
1530
  projectName,
1712
- projectContext: renderFillScaffold()
1531
+ projectContext: renderFillScaffold(),
1532
+ // codex 와 **같은 파일**(프로젝트 루트 `AGENTS.md`)이다. 두 transform 이 서로 다른 본문을
1533
+ // 쓰면 나중에 도는 쪽이 앞선 쪽을 덮어써, codex+opencode 조합에서 룰이 통째로 사라진다
1534
+ // (독립 검증 C-1 실측). 같은 내용을 쓰면 순서가 결과를 바꾸지 않는다.
1535
+ harnessRules: renderRulesBlock(portRules(harnessRoot, rules))
1713
1536
  });
1714
1537
  writer.write(agentsMdPath, agentsMdOut);
1715
- const opencodeJsonPath = join7(projectDir, "opencode.json");
1538
+ const opencodeJsonPath = join6(projectDir, "opencode.json");
1716
1539
  writer.write(opencodeJsonPath, renderOpencodeJson({ template: opencodeTemplate, mcp }));
1717
- const cmdDir = join7(projectDir, ".opencode/commands");
1540
+ const cmdDir = join6(projectDir, ".opencode/commands");
1718
1541
  if (!refreshOnly) ensureDir(cmdDir);
1719
1542
  const commandFiles = [];
1720
1543
  for (const id of selectedInternalSkills) {
1721
- const src = join7(harnessRoot, "templates/skills", id, "SKILL.md");
1722
- if (!existsSync8(src)) {
1544
+ const src = join6(harnessRoot, "templates/skills", id, "SKILL.md");
1545
+ if (!existsSync7(src)) {
1723
1546
  continue;
1724
1547
  }
1725
- const target = join7(cmdDir, `${id}.md`);
1726
- const shellDependent = existsSync8(join7(harnessRoot, "templates/skills", id, "scripts"));
1548
+ const target = join6(cmdDir, `${id}.md`);
1549
+ const shellDependent = existsSync7(join6(harnessRoot, "templates/skills", id, "scripts"));
1727
1550
  const wrote = writer.write(
1728
1551
  target,
1729
- renderCommandFromSkill(readFileSync7(src, "utf8"), id, { shellDependent })
1552
+ renderCommandFromSkill(readFileSync6(src, "utf8"), id, { shellDependent })
1730
1553
  );
1731
1554
  if (!wrote) continue;
1732
1555
  commandFiles.push(target);
@@ -1739,17 +1562,17 @@ function runOpencodeTransform(params) {
1739
1562
  };
1740
1563
  }
1741
1564
  function readRequired2(path) {
1742
- if (!existsSync8(path)) {
1565
+ if (!existsSync7(path)) {
1743
1566
  throw new Error(`OpenCode transform: required source missing: ${path}`);
1744
1567
  }
1745
- return readFileSync7(path, "utf8");
1568
+ return readFileSync6(path, "utf8");
1746
1569
  }
1747
1570
  function readOptionalJson2(path) {
1748
- if (!existsSync8(path)) {
1571
+ if (!existsSync7(path)) {
1749
1572
  return null;
1750
1573
  }
1751
1574
  try {
1752
- return JSON.parse(readFileSync7(path, "utf8"));
1575
+ return JSON.parse(readFileSync6(path, "utf8"));
1753
1576
  } catch {
1754
1577
  return null;
1755
1578
  }
@@ -1763,6 +1586,7 @@ function runCliTransforms(params) {
1763
1586
  projectDir,
1764
1587
  cli: cli2,
1765
1588
  selectedInternalSkills,
1589
+ rules,
1766
1590
  previousExternal,
1767
1591
  refreshOnly = false,
1768
1592
  codexTrust = false
@@ -1788,6 +1612,7 @@ function runCliTransforms(params) {
1788
1612
  harnessRoot,
1789
1613
  projectDir,
1790
1614
  selectedInternalSkills,
1615
+ rules,
1791
1616
  baseline,
1792
1617
  refreshOnly
1793
1618
  });
@@ -1802,6 +1627,7 @@ function runCliTransforms(params) {
1802
1627
  harnessRoot,
1803
1628
  projectDir,
1804
1629
  selectedInternalSkills,
1630
+ rules,
1805
1631
  baseline,
1806
1632
  refreshOnly
1807
1633
  });
@@ -1813,6 +1639,7 @@ function runCliTransforms(params) {
1813
1639
  harnessRoot,
1814
1640
  projectDir,
1815
1641
  selectedInternalSkills,
1642
+ rules,
1816
1643
  baseline,
1817
1644
  refreshOnly
1818
1645
  });
@@ -1832,8 +1659,8 @@ function runCliTransforms(params) {
1832
1659
 
1833
1660
  // src/env-files.ts
1834
1661
  init_esm_shims();
1835
- import { appendFileSync, existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
1836
- import { join as join8 } from "path";
1662
+ import { appendFileSync, existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
1663
+ import { join as join7 } from "path";
1837
1664
  var ENV_EXAMPLE_BODY = `# .env.example \u2014 csr-supabase Track
1838
1665
  # Copy to .env (gitignored) and fill in values: cp .env.example .env
1839
1666
 
@@ -1870,19 +1697,19 @@ function writeEnvExample(projectDir, tracks) {
1870
1697
  if (!tracks.some((t) => ENV_EXAMPLE_TRACKS.includes(t))) {
1871
1698
  return false;
1872
1699
  }
1873
- const path = join8(projectDir, ".env.example");
1874
- if (existsSync9(path)) {
1700
+ const path = join7(projectDir, ".env.example");
1701
+ if (existsSync8(path)) {
1875
1702
  return false;
1876
1703
  }
1877
- writeFileSync4(path, ENV_EXAMPLE_BODY);
1704
+ writeFileSync3(path, ENV_EXAMPLE_BODY);
1878
1705
  return true;
1879
1706
  }
1880
1707
  function addGitignoreEnv(projectDir) {
1881
- const path = join8(projectDir, ".gitignore");
1882
- if (!existsSync9(path)) {
1708
+ const path = join7(projectDir, ".gitignore");
1709
+ if (!existsSync8(path)) {
1883
1710
  return false;
1884
1711
  }
1885
- const content = readFileSync8(path, "utf8");
1712
+ const content = readFileSync7(path, "utf8");
1886
1713
  if (GITIGNORE_ENV_PATTERN.test(content)) {
1887
1714
  return false;
1888
1715
  }
@@ -1896,11 +1723,11 @@ function addGitignoreEnv(projectDir) {
1896
1723
  var AGENT_ARTIFACT_DIRS = [".factory/", ".goose/", ".uzys-agent-harness/"];
1897
1724
  var GITIGNORE_AGENT_ARTIFACT_HEADER = "# agent CLI / harness \uC790\uB3D9 \uC0DD\uC131\uBB3C (auto-added by agent-harness)";
1898
1725
  function addGitignoreAgentArtifacts(projectDir) {
1899
- const path = join8(projectDir, ".gitignore");
1900
- if (!existsSync9(path)) {
1726
+ const path = join7(projectDir, ".gitignore");
1727
+ if (!existsSync8(path)) {
1901
1728
  return [];
1902
1729
  }
1903
- const content = readFileSync8(path, "utf8");
1730
+ const content = readFileSync7(path, "utf8");
1904
1731
  const missing = AGENT_ARTIFACT_DIRS.filter((pattern) => {
1905
1732
  const lineRegex = new RegExp(`^${pattern.replace(/\./g, "\\.").replace(/\//g, "/")}\\s*$`, "m");
1906
1733
  return !lineRegex.test(content);
@@ -1915,43 +1742,13 @@ ${block}
1915
1742
  `);
1916
1743
  return [...missing];
1917
1744
  }
1918
- function writeMcpAllowlist(projectDir) {
1919
- const allowlistPath = join8(projectDir, ".mcp-allowlist");
1920
- if (existsSync9(allowlistPath)) {
1921
- return null;
1922
- }
1923
- const mcpPath = join8(projectDir, ".mcp.json");
1924
- if (!existsSync9(mcpPath)) {
1925
- return null;
1926
- }
1927
- let names = [];
1928
- try {
1929
- const parsed = JSON.parse(readFileSync8(mcpPath, "utf8"));
1930
- names = Object.keys(parsed.mcpServers ?? {}).sort();
1931
- } catch {
1932
- return null;
1933
- }
1934
- if (names.length === 0) {
1935
- return [];
1936
- }
1937
- const body = [
1938
- "# MCP Server Allowlist \u2014 auto-generated by agent-harness install",
1939
- "# Referenced by mcp-pre-exec.sh hook. Remove or '#' comment any server you want to block.",
1940
- "# Deleting this file disables the gate (allows all MCP calls).",
1941
- "",
1942
- ...names,
1943
- ""
1944
- ].join("\n");
1945
- writeFileSync4(allowlistPath, body);
1946
- return names;
1947
- }
1948
1745
 
1949
1746
  // src/external-installer.ts
1950
1747
  init_esm_shims();
1951
1748
  import { spawnSync } from "child_process";
1952
- import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync9 } from "fs";
1749
+ import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync8 } from "fs";
1953
1750
  import { homedir as homedir2 } from "os";
1954
- import { join as join9 } from "path";
1751
+ import { join as join8 } from "path";
1955
1752
  var DEFAULT_SPAWN_TIMEOUT_MS = 12e4;
1956
1753
  function selectExternalTargets(assets, ctx) {
1957
1754
  const conditionPassed = filterApplicableAssets(assets, ctx).filter(
@@ -2027,8 +1824,8 @@ function installOne(asset, ctx) {
2027
1824
  cwd
2028
1825
  );
2029
1826
  case "shell-script": {
2030
- const scriptPath = join9(ctx.harnessRoot, method.script);
2031
- if (!existsSync10(scriptPath)) {
1827
+ const scriptPath = join8(ctx.harnessRoot, method.script);
1828
+ if (!existsSync9(scriptPath)) {
2032
1829
  return {
2033
1830
  asset,
2034
1831
  ok: false,
@@ -2118,17 +1915,17 @@ function detectVersion(method, spawn) {
2118
1915
  if (at2 <= 0) return void 0;
2119
1916
  const plugin = method.pluginId.slice(0, at2);
2120
1917
  const marketplaceShort = method.pluginId.slice(at2 + 1);
2121
- const cacheBase = join9(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
2122
- if (!existsSync10(cacheBase)) return void 0;
2123
- const versions = readdirSync2(cacheBase).filter((v2) => /^\d/.test(v2)).sort();
1918
+ const cacheBase = join8(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
1919
+ if (!existsSync9(cacheBase)) return void 0;
1920
+ const versions = readdirSync(cacheBase).filter((v2) => /^\d/.test(v2)).sort();
2124
1921
  return versions.at(-1);
2125
1922
  }
2126
1923
  case "npm": {
2127
1924
  const npmRoot = getNpmGlobalRoot(spawn);
2128
1925
  if (!npmRoot) return void 0;
2129
- const pkgJson = join9(npmRoot, method.pkg, "package.json");
2130
- if (!existsSync10(pkgJson)) return void 0;
2131
- const parsed = JSON.parse(readFileSync9(pkgJson, "utf8"));
1926
+ const pkgJson = join8(npmRoot, method.pkg, "package.json");
1927
+ if (!existsSync9(pkgJson)) return void 0;
1928
+ const parsed = JSON.parse(readFileSync8(pkgJson, "utf8"));
2132
1929
  return parsed.version;
2133
1930
  }
2134
1931
  default:
@@ -2155,7 +1952,7 @@ function getNpmGlobalRoot(spawn) {
2155
1952
 
2156
1953
  // src/mcp-merge.ts
2157
1954
  init_esm_shims();
2158
- import { existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
1955
+ import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
2159
1956
  function parseTrackMcpMap(raw) {
2160
1957
  const rows = [];
2161
1958
  for (const line of raw.split(/\r?\n/)) {
@@ -2206,15 +2003,15 @@ function mergeMcpServers(base, rows, tracks) {
2206
2003
  return out;
2207
2004
  }
2208
2005
  function composeMcpJson(opts) {
2209
- const base = JSON.parse(readFileSync10(opts.templateMcpPath, "utf8"));
2210
- const merged = opts.existingPath && existsSync11(opts.existingPath) ? mergeUserBase(base, opts.existingPath) : base;
2211
- const mapRaw = existsSync11(opts.trackMapPath) ? readFileSync10(opts.trackMapPath, "utf8") : "";
2006
+ const base = JSON.parse(readFileSync9(opts.templateMcpPath, "utf8"));
2007
+ const merged = opts.existingPath && existsSync10(opts.existingPath) ? mergeUserBase(base, opts.existingPath) : base;
2008
+ const mapRaw = existsSync10(opts.trackMapPath) ? readFileSync9(opts.trackMapPath, "utf8") : "";
2212
2009
  const rows = parseTrackMcpMap(mapRaw);
2213
2010
  return mergeMcpServers(merged, rows, opts.tracks);
2214
2011
  }
2215
2012
  function mergeUserBase(base, existingPath) {
2216
2013
  try {
2217
- const existing = JSON.parse(readFileSync10(existingPath, "utf8"));
2014
+ const existing = JSON.parse(readFileSync9(existingPath, "utf8"));
2218
2015
  return {
2219
2016
  ...base,
2220
2017
  mcpServers: { ...base.mcpServers, ...existing.mcpServers }
@@ -2224,23 +2021,58 @@ function mergeUserBase(base, existingPath) {
2224
2021
  }
2225
2022
  }
2226
2023
  function writeMcpJson(path, mcp) {
2227
- writeFileSync5(path, `${JSON.stringify(mcp, null, 2)}
2024
+ writeFileSync4(path, `${JSON.stringify(mcp, null, 2)}
2228
2025
  `);
2229
2026
  }
2230
2027
 
2028
+ // src/superseded.ts
2029
+ init_esm_shims();
2030
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
2031
+ import { join as join9 } from "path";
2032
+ function sameTracks(a, b2) {
2033
+ if (a.length !== b2.length) return false;
2034
+ const sa = [...a].sort();
2035
+ const sb = [...b2].sort();
2036
+ return sa.every((t, i) => t === sb[i]);
2037
+ }
2038
+ function policyKey(claudeRelative) {
2039
+ return POLICY_DIRS.some(
2040
+ ({ dir, ext }) => claudeRelative.startsWith(`${dir}/`) && claudeRelative.endsWith(ext)
2041
+ ) ? claudeRelative : null;
2042
+ }
2043
+ function findSuperseded(projectDir, spec, previous) {
2044
+ if (!previous) return [];
2045
+ if (!sameTracks(previous.spec.tracks, spec.tracks)) return [];
2046
+ const baseline = new Map((previous.policyFiles ?? []).map((f) => [f.path, f.sha256]));
2047
+ if (baseline.size === 0) return [];
2048
+ const out = [];
2049
+ for (const entry of buildManifest(spec)) {
2050
+ if (entry.applies(spec)) continue;
2051
+ const id = classifyBaselineTarget(entry.target)?.id;
2052
+ if (!id) continue;
2053
+ const key = policyKey(entry.target.slice(".claude/".length));
2054
+ if (!key) continue;
2055
+ const abs = join9(projectDir, entry.target);
2056
+ if (!existsSync11(abs)) continue;
2057
+ if (!isHarnessOwned(baseline, key, readFileSync10(abs, "utf-8"))) continue;
2058
+ out.push({ target: entry.target, id, source: entry.source });
2059
+ }
2060
+ return out;
2061
+ }
2062
+
2231
2063
  // src/update-mode.ts
2232
2064
  init_esm_shims();
2233
2065
  import {
2234
- copyFileSync as copyFileSync2,
2066
+ copyFileSync,
2235
2067
  existsSync as existsSync12,
2236
2068
  lstatSync,
2237
- mkdirSync as mkdirSync5,
2238
- readdirSync as readdirSync3,
2069
+ mkdirSync as mkdirSync3,
2070
+ readdirSync as readdirSync2,
2239
2071
  readFileSync as readFileSync11,
2240
2072
  unlinkSync,
2241
- writeFileSync as writeFileSync6
2073
+ writeFileSync as writeFileSync5
2242
2074
  } from "fs";
2243
- import { basename as basename4, dirname as dirname5, join as join10 } from "path";
2075
+ import { basename as basename4, dirname as dirname3, join as join10 } from "path";
2244
2076
  var LEGACY_ANCHOR_FILE = ".claude/CLAUDE.md";
2245
2077
  function buildUpdateSpec(projectDir, tracks) {
2246
2078
  return {
@@ -2267,7 +2099,8 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot) {
2267
2099
  externalBackedUp: [],
2268
2100
  installedNew: [],
2269
2101
  restored: [],
2270
- needsReinstall: []
2102
+ needsReinstall: [],
2103
+ mcpAllowlistRetired: null
2271
2104
  };
2272
2105
  const fresh = installNewAssets(projectDir, templatesDir);
2273
2106
  report.installedNew = fresh.installed;
@@ -2294,11 +2127,15 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot) {
2294
2127
  report.skillsBackedUp = skillSync.backedUp;
2295
2128
  report.skillsSkippedLinks = skillSync.skippedLinks;
2296
2129
  refreshSkillBaseline(projectDir);
2297
- syncHarnessAnchor(projectDir, templatesDir, report);
2130
+ const installLog = readInstallLog(projectDir);
2131
+ if (existsSync12(claudeDir) && (installLog === null || installLog.spec.cli.includes("claude"))) {
2132
+ syncHarnessAnchor(projectDir, templatesDir, report);
2133
+ }
2298
2134
  const settingsPath = join10(claudeDir, "settings.json");
2299
2135
  if (existsSync12(settingsPath)) {
2300
2136
  report.staleHookRefs = cleanStaleHookRefs(settingsPath, claudeDir);
2301
2137
  }
2138
+ report.mcpAllowlistRetired = retireMcpAllowlist(projectDir);
2302
2139
  const external = refreshExternalCli(projectDir, harnessRoot);
2303
2140
  report.externalUpdated = external.externalUpdated;
2304
2141
  report.externalBackedUp = external.externalBackedUp;
@@ -2308,10 +2145,13 @@ function installNewAssets(projectDir, templatesDir) {
2308
2145
  const installed = [];
2309
2146
  const restored = [];
2310
2147
  const needsReinstall = [];
2311
- const claudeSelected = readInstallLog(projectDir)?.spec.cli.includes("claude") ?? false;
2148
+ const log = readInstallLog(projectDir);
2149
+ const claudeSelected = log?.spec.cli.includes("claude") ?? false;
2150
+ const baselineExcluded = new Set(log?.spec.baselineExclude ?? []);
2312
2151
  const priorBaseline = policyBaseline(projectDir);
2313
2152
  for (const entry of trackOnlyFileAssets(installedTracks(projectDir))) {
2314
2153
  if (entry.target === HARNESS_ANCHOR_FILE) continue;
2154
+ if (isBaselineExcluded(entry.target, baselineExcluded)) continue;
2315
2155
  const target = join10(projectDir, entry.target);
2316
2156
  if (existsSync12(target)) continue;
2317
2157
  const source = join10(templatesDir, entry.source);
@@ -2323,8 +2163,8 @@ function installNewAssets(projectDir, templatesDir) {
2323
2163
  continue;
2324
2164
  }
2325
2165
  }
2326
- mkdirSync5(dirname5(target), { recursive: true });
2327
- copyFileSync2(source, target);
2166
+ mkdirSync3(dirname3(target), { recursive: true });
2167
+ copyFileSync(source, target);
2328
2168
  const recordedAs = entry.target.startsWith(".claude/") ? entry.target.slice(8) : entry.target;
2329
2169
  if (priorBaseline.has(recordedAs)) restored.push(entry.target);
2330
2170
  else installed.push(entry.target);
@@ -2347,7 +2187,7 @@ function syncHarnessAnchor(projectDir, templatesDir, report) {
2347
2187
  if (!existsSync12(templateMd)) return;
2348
2188
  const anchor = join10(projectDir, HARNESS_ANCHOR_FILE);
2349
2189
  const existed = existsSync12(anchor);
2350
- copyFileSync2(templateMd, anchor);
2190
+ copyFileSync(templateMd, anchor);
2351
2191
  if (existed) {
2352
2192
  report.claudeMdUpdated = true;
2353
2193
  return;
@@ -2364,7 +2204,7 @@ function upsertRootImport(projectDir) {
2364
2204
  tracks: installedTracks(projectDir)
2365
2205
  });
2366
2206
  if (next === existing) return false;
2367
- writeFileSync6(target, next);
2207
+ writeFileSync5(target, next);
2368
2208
  return true;
2369
2209
  }
2370
2210
  function installedTracks(projectDir) {
@@ -2391,11 +2231,14 @@ function recordAnchorBaseline(projectDir, anchor) {
2391
2231
  }
2392
2232
  function refreshExternalCli(projectDir, harnessRoot) {
2393
2233
  const log = readInstallLog(projectDir);
2234
+ const baselineExcluded = new Set(log?.spec.baselineExclude ?? []);
2394
2235
  const result = runCliTransforms({
2395
2236
  harnessRoot,
2396
2237
  projectDir,
2397
2238
  cli: ALL_CLI_TARGETS,
2398
2239
  selectedInternalSkills: INTERNAL_BUNDLED_SKILL_IDS,
2240
+ // 스킬은 **전부** 넘긴다 — 안 깔린 스킬은 대상 파일이 없어 refreshOnly 가 건너뛴다.
2241
+ rules: ALL_RULES.filter((r) => !isBaselineExcluded(`.claude/rules/${r}.md`, baselineExcluded)),
2399
2242
  previousExternal: log?.externalFiles ?? [],
2400
2243
  refreshOnly: true
2401
2244
  });
@@ -2421,7 +2264,7 @@ function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2421
2264
  if (!existsSync12(target) || !existsSync12(source)) return { updated: 0, backedUp: [] };
2422
2265
  let updated = 0;
2423
2266
  const backedUp = [];
2424
- for (const file of readdirSync3(target)) {
2267
+ for (const file of readdirSync2(target)) {
2425
2268
  if (!file.endsWith(ext)) continue;
2426
2269
  const targetFile = join10(target, file);
2427
2270
  const sourceFile = join10(source, file);
@@ -2433,7 +2276,7 @@ function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2433
2276
  backupFile(targetFile, now);
2434
2277
  backedUp.push(`${ctx.prefix}/${file}`);
2435
2278
  }
2436
- writeFileSync6(targetFile, next);
2279
+ writeFileSync5(targetFile, next);
2437
2280
  updated++;
2438
2281
  }
2439
2282
  return { updated, backedUp };
@@ -2444,7 +2287,7 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
2444
2287
  let updated = 0;
2445
2288
  const backedUp = [];
2446
2289
  const skippedLinks = [];
2447
- for (const skill of readdirSync3(sourceDir, { withFileTypes: true })) {
2290
+ for (const skill of readdirSync2(sourceDir, { withFileTypes: true })) {
2448
2291
  if (!skill.isDirectory()) continue;
2449
2292
  const targetSkill = join10(targetDir, skill.name);
2450
2293
  if (!existsSync12(targetSkill)) continue;
@@ -2456,8 +2299,8 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
2456
2299
  const targetFile = join10(targetSkill, rel);
2457
2300
  const next = readFileSync11(join10(sourceDir, skill.name, rel), "utf8");
2458
2301
  if (!existsSync12(targetFile)) {
2459
- mkdirSync5(dirname5(targetFile), { recursive: true });
2460
- writeFileSync6(targetFile, next);
2302
+ mkdirSync3(dirname3(targetFile), { recursive: true });
2303
+ writeFileSync5(targetFile, next);
2461
2304
  updated++;
2462
2305
  continue;
2463
2306
  }
@@ -2468,7 +2311,7 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
2468
2311
  backupFile(targetFile, now);
2469
2312
  backedUp.push(`${skill.name}/${rel}`);
2470
2313
  }
2471
- writeFileSync6(targetFile, next);
2314
+ writeFileSync5(targetFile, next);
2472
2315
  updated++;
2473
2316
  }
2474
2317
  }
@@ -2509,7 +2352,7 @@ function refreshSkillBaseline(projectDir) {
2509
2352
  function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2510
2353
  if (!existsSync12(target) || !existsSync12(source)) return [];
2511
2354
  const removed = [];
2512
- for (const file of readdirSync3(target)) {
2355
+ for (const file of readdirSync2(target)) {
2513
2356
  if (!file.endsWith(ext)) continue;
2514
2357
  const sourceFile = join10(source, file);
2515
2358
  if (!existsSync12(sourceFile)) {
@@ -2526,6 +2369,17 @@ function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()
2526
2369
  }
2527
2370
  return removed;
2528
2371
  }
2372
+ function retireMcpAllowlist(projectDir, now = /* @__PURE__ */ new Date()) {
2373
+ const target = join10(projectDir, ".mcp-allowlist");
2374
+ if (!existsSync12(target)) return null;
2375
+ try {
2376
+ const backup = backupFile(target, now);
2377
+ unlinkSync(target);
2378
+ return backup;
2379
+ } catch {
2380
+ return null;
2381
+ }
2382
+ }
2529
2383
  function cleanStaleHookRefs(settingsPath, claudeDir) {
2530
2384
  let settings;
2531
2385
  try {
@@ -2548,7 +2402,7 @@ function cleanStaleHookRefs(settingsPath, claudeDir) {
2548
2402
  }
2549
2403
  if (removed.length > 0) {
2550
2404
  const next = { ...settings, hooks: cleanedHooks };
2551
- writeFileSync6(settingsPath, `${JSON.stringify(next, null, 2)}
2405
+ writeFileSync5(settingsPath, `${JSON.stringify(next, null, 2)}
2552
2406
  `);
2553
2407
  }
2554
2408
  return removed;
@@ -2588,17 +2442,27 @@ function runInstall(ctx) {
2588
2442
  throw new Error(`Templates dir not found: ${templatesDir}`);
2589
2443
  }
2590
2444
  const claudeDir = join11(projectDir, ".claude");
2591
- if (mode === "update" && !existsSync13(claudeDir)) {
2592
- throw new Error(`Update mode requires existing .claude/ at ${claudeDir}`);
2593
- }
2594
2445
  const previousLog = readInstallLog(projectDir);
2446
+ const claudeWasSelected = previousLog?.spec.cli.includes("claude") ?? false;
2447
+ if (mode === "update" && !existsSync13(claudeDir) && (previousLog === null || claudeWasSelected)) {
2448
+ throw new Error(
2449
+ 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}`
2450
+ );
2451
+ }
2595
2452
  const backupPath = resolveBackupPath(ctx, mode, claudeDir);
2596
2453
  if (mode === "update") {
2597
2454
  return runUpdateInstall(ctx, templatesDir, backupPath);
2598
2455
  }
2599
2456
  const manifestSpec = buildManifestSpec(spec);
2600
2457
  const policyBase = new Map((previousLog?.policyFiles ?? []).map((f) => [f.path, f.sha256]));
2601
- const base = spec.cli.includes("claude") ? installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase) : emptyClaudeBaseline();
2458
+ const baselineExcluded = new Set(spec.baselineExclude ?? []);
2459
+ const supersededFound = findSuperseded(projectDir, manifestSpec, previousLog);
2460
+ const base = spec.cli.includes("claude") ? installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase, baselineExcluded) : (
2461
+ // claude 미선택이어도 CLI 중립 자산은 깔린다. manifest 전체가 `.claude/` baseline 안에서만
2462
+ // 돌던 탓에 이 자산들이 claude 설치에만 도달했는데, **배포 룰 본문이 이 스크립트들을
2463
+ // 호출 지점으로 지목한다** — 즉 없는 도구를 있다고 안내하고 있었다(#300 과 같은 형태).
2464
+ installCliNeutralAssets(manifestSpec, projectDir, templatesDir, baselineExcluded)
2465
+ );
2602
2466
  const mcpResult = composeAndWriteMcp(harnessRoot, projectDir, spec);
2603
2467
  const ciScaffold = isAssetSelected("ci-scaffold", {
2604
2468
  tracks: spec.tracks,
@@ -2616,6 +2480,11 @@ function runInstall(ctx) {
2616
2480
  projectDir,
2617
2481
  cli: spec.cli,
2618
2482
  selectedInternalSkills: manifestSpec.selectedInternalSkills,
2483
+ // 룰 목록의 SSOT 는 하나다 — `.claude/rules/` 를 채우는 것과 같은 `resolveRules` 결과가
2484
+ // 나머지 세 CLI 로도 간다. 여기서 다시 고르면 CLI 마다 다른 룰이 깔린다.
2485
+ rules: resolveRules(manifestSpec).filter(
2486
+ (r) => !isBaselineExcluded(`.claude/rules/${r}.md`, baselineExcluded)
2487
+ ),
2619
2488
  previousExternal: previousLog?.externalFiles ?? [],
2620
2489
  codexTrust: (spec.scope ?? "project") === "global" && spec.options.withCodexTrust
2621
2490
  });
@@ -2635,7 +2504,12 @@ function runInstall(ctx) {
2635
2504
  rootClaudeMd: base.rootClaudeMd,
2636
2505
  // 외부 CLI 백업도 같은 줄에 노출한다 — 백업이 화면에 안 보이면 사용자는 자기 편집분이
2637
2506
  // 어디 갔는지 알 수 없고, 그러면 백업은 있어도 없는 것과 같다 (ADR-046/047 과 같은 이유).
2638
- backups: [...base.backups, ...externalBackups]
2507
+ backups: [...base.backups, ...externalBackups],
2508
+ baselineExcluded: base.excluded,
2509
+ baselineExcludedOnDisk: base.excludedOnDisk,
2510
+ // 밀려난 자산 정리는 **manifest 복사가 끝난 뒤**에 한다. 앞에서 지우면 이번 설치가 다시
2511
+ // 깔 수도 있는 파일을 지웠다 되돌리는 셈이라, 무엇이 최종 상태인지 보고가 흐려진다.
2512
+ superseded: applySupersededCleanup(spec, projectDir, supersededFound)
2639
2513
  };
2640
2514
  ctx.onProgress?.({ type: "baseline-complete", baseline });
2641
2515
  const external = runExternalPhase(ctx);
@@ -2651,6 +2525,21 @@ function runInstall(ctx) {
2651
2525
  );
2652
2526
  return { ...baseline, external, staleHookRefs };
2653
2527
  }
2528
+ function applySupersededCleanup(spec, projectDir, found) {
2529
+ if (found.length === 0) return { removed: [], kept: [] };
2530
+ if (!spec.cleanSuperseded) return { removed: [], kept: found.map((f) => f.target) };
2531
+ const removed = [];
2532
+ const kept = [];
2533
+ for (const asset of found) {
2534
+ try {
2535
+ unlinkSync2(join11(projectDir, asset.target));
2536
+ removed.push(asset.target);
2537
+ } catch {
2538
+ kept.push(asset.target);
2539
+ }
2540
+ }
2541
+ return { removed, kept };
2542
+ }
2654
2543
  function healStaleHookRefs(spec, projectDir) {
2655
2544
  if (!spec.cli.includes("claude")) return [];
2656
2545
  const settingsPath = join11(projectDir, ".claude/settings.json");
@@ -2668,6 +2557,11 @@ function runUpdateInstall(ctx, templatesDir, backupPath) {
2668
2557
  filesCopied: 0,
2669
2558
  dirsCopied: 0,
2670
2559
  skipped: 0,
2560
+ baselineExcluded: [],
2561
+ baselineExcludedOnDisk: [],
2562
+ // update 는 밀려난 자산을 다루지 않는다 — 그 판정은 사용자가 옵션을 고르는 설치 경로의
2563
+ // 것이고, update 는 무엇을 골랐는지 다시 묻지 않는다.
2564
+ superseded: { removed: [], kept: [] },
2671
2565
  backup: backupPath,
2672
2566
  installedTracks: [...ctx.spec.tracks].sort(),
2673
2567
  mcpServers: [],
@@ -2681,7 +2575,6 @@ function runUpdateInstall(ctx, templatesDir, backupPath) {
2681
2575
  envFiles: {
2682
2576
  envExampleCreated: false,
2683
2577
  gitignoreEnvAdded: false,
2684
- mcpAllowlist: null,
2685
2578
  gitignoreNpxSkillsAdded: []
2686
2579
  },
2687
2580
  rootClaudeMd: null
@@ -2718,9 +2611,30 @@ function emptyClaudeBaseline() {
2718
2611
  categories: { rules: [], agents: [], hooks: [], commands: 0, skills: [] },
2719
2612
  rootClaudeMd: null,
2720
2613
  rootClaudeMdLog: null,
2721
- backups: []
2614
+ backups: [],
2615
+ excluded: [],
2616
+ excludedOnDisk: []
2722
2617
  };
2723
2618
  }
2619
+ function installCliNeutralAssets(manifestSpec, projectDir, templatesDir, baselineExcluded) {
2620
+ const result = emptyClaudeBaseline();
2621
+ for (const entry of buildManifest(manifestSpec)) {
2622
+ if (!entry.applies(manifestSpec)) continue;
2623
+ if (entry.target.startsWith(".claude/rules/") && isBaselineExcluded(entry.target, baselineExcluded)) {
2624
+ result.excluded.push(entry.target);
2625
+ continue;
2626
+ }
2627
+ if (!isCliNeutralTarget(entry.target)) continue;
2628
+ const source = join11(templatesDir, entry.source);
2629
+ if (!existsSync13(source)) {
2630
+ result.skipped += 1;
2631
+ continue;
2632
+ }
2633
+ copyFile(source, join11(projectDir, entry.target));
2634
+ result.filesCopied += 1;
2635
+ }
2636
+ return result;
2637
+ }
2724
2638
  function backupEditedPolicyFile(entryTarget, target, source, baseline) {
2725
2639
  const prefix = ".claude/";
2726
2640
  if (!entryTarget.startsWith(prefix)) return null;
@@ -2735,7 +2649,7 @@ function backupEditedPolicyFile(entryTarget, target, source, baseline) {
2735
2649
  if (recorded !== void 0 && recorded === hashContent(current)) return null;
2736
2650
  return backupFile(target);
2737
2651
  }
2738
- function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase) {
2652
+ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase, baselineExcluded) {
2739
2653
  ensureProjectSkeleton(projectDir);
2740
2654
  const result = emptyClaudeBaseline();
2741
2655
  const manifest = buildManifest(manifestSpec);
@@ -2743,8 +2657,13 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
2743
2657
  if (!entry.applies(manifestSpec)) {
2744
2658
  continue;
2745
2659
  }
2746
- const source = join11(templatesDir, entry.source);
2747
2660
  const target = join11(projectDir, entry.target);
2661
+ if (isBaselineExcluded(entry.target, baselineExcluded)) {
2662
+ result.excluded.push(entry.target);
2663
+ if (existsSync13(target)) result.excludedOnDisk.push(entry.target);
2664
+ continue;
2665
+ }
2666
+ const source = join11(templatesDir, entry.source);
2748
2667
  if (!existsSync13(source)) {
2749
2668
  result.skipped += 1;
2750
2669
  continue;
@@ -2788,7 +2707,6 @@ function writeEnvironmentFiles(projectDir, tracks) {
2788
2707
  return {
2789
2708
  envExampleCreated: writeEnvExample(projectDir, tracks),
2790
2709
  gitignoreEnvAdded: addGitignoreEnv(projectDir),
2791
- mcpAllowlist: writeMcpAllowlist(projectDir),
2792
2710
  // v0.8.0 — `.factory/`, `.goose/` ignore (npx skills universal install 사용자 #3).
2793
2711
  // 2026-08-02 — `.uzys-agent-harness/` 합류 (설치 로그 + 훅 차단 로그).
2794
2712
  gitignoreNpxSkillsAdded: addGitignoreAgentArtifacts(projectDir)
@@ -2880,13 +2798,6 @@ function collectRootFiles(envFiles, ciScaffold, mcpCreated) {
2880
2798
  if (envFiles.envExampleCreated) {
2881
2799
  files.push({ path: ".env.example", change: "created", notes: ["Supabase \uD1A0\uD070 \uAC00\uC774\uB4DC"] });
2882
2800
  }
2883
- if (envFiles.mcpAllowlist && envFiles.mcpAllowlist.length > 0) {
2884
- files.push({
2885
- path: ".mcp-allowlist",
2886
- change: "created",
2887
- notes: [`MCP allowlist (${envFiles.mcpAllowlist.length} server)`]
2888
- });
2889
- }
2890
2801
  const gitignoreAdded = [
2891
2802
  ...envFiles.gitignoreEnvAdded ? [".env"] : [],
2892
2803
  ...envFiles.gitignoreNpxSkillsAdded
@@ -2923,9 +2834,9 @@ function accumulateCategory(cats, entry) {
2923
2834
  }
2924
2835
  function writeInstalledTracks(projectDir, tracks) {
2925
2836
  const path = join11(projectDir, ".claude/.installed-tracks");
2926
- mkdirSync6(dirname6(path), { recursive: true });
2837
+ mkdirSync4(dirname4(path), { recursive: true });
2927
2838
  const sorted = [...new Set(tracks)].sort().join("\n");
2928
- writeFileSync7(path, `${sorted}
2839
+ writeFileSync6(path, `${sorted}
2929
2840
  `);
2930
2841
  }
2931
2842
  function writeRootClaudeMd(projectDir, tracks) {
@@ -2933,7 +2844,7 @@ function writeRootClaudeMd(projectDir, tracks) {
2933
2844
  const existing = existsSync13(target) ? readFileSync12(target, "utf-8") : null;
2934
2845
  const content = upsertHarnessImport(existing, { projectName: basename5(projectDir), tracks });
2935
2846
  if (content !== existing) {
2936
- writeFileSync7(target, content);
2847
+ writeFileSync6(target, content);
2937
2848
  }
2938
2849
  return { created: existing === null };
2939
2850
  }
@@ -2946,7 +2857,7 @@ function chmodHooksSync(hookDir) {
2946
2857
  }
2947
2858
  }
2948
2859
  function listHookFiles(hookDir) {
2949
- return readdirSync4(hookDir, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".sh")).map((e2) => resolve(hookDir, e2.name));
2860
+ return readdirSync3(hookDir, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".sh")).map((e2) => resolve(hookDir, e2.name));
2950
2861
  }
2951
2862
 
2952
2863
  // src/commands/install-render.ts
@@ -3299,6 +3210,15 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
3299
3210
  )
3300
3211
  );
3301
3212
  }
3213
+ if (baseline.updateMode.mcpAllowlistRetired) {
3214
+ log(
3215
+ assetRow(
3216
+ "skip",
3217
+ ".mcp-allowlist",
3218
+ `retired \xB7 the hook that read it is gone \u2014 backed up as ${shortenPath(baseline.updateMode.mcpAllowlistRetired)}`
3219
+ )
3220
+ );
3221
+ }
3302
3222
  if (baseline.updateMode.policyBackedUp.length > 0) {
3303
3223
  log(
3304
3224
  assetRow(
@@ -3373,12 +3293,12 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
3373
3293
  phase1Row(
3374
3294
  "hooks",
3375
3295
  cats.hooks.length,
3376
- "session-start \xB7 protect-files \xB7 mcp-pre-exec (security) \xB7 task-brief-nudge",
3296
+ "session-start \xB7 protect-files \xB7 task-brief-nudge",
3377
3297
  cats.hooks
3378
3298
  );
3379
3299
  }
3380
3300
  if (cats.commands > 0) {
3381
- phase1Row("commands", cats.commands, "/ecc:* (ECC plugin OFF fallback)");
3301
+ phase1Row("commands", cats.commands, "slash commands");
3382
3302
  }
3383
3303
  if (cats.skills.length > 0) {
3384
3304
  phase1Row(
@@ -3392,6 +3312,38 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
3392
3312
  log(assetRow("success", "rules + hooks + commands + agents", `${baseline.filesCopied} files`));
3393
3313
  log(assetRow("success", "skeleton", `${baseline.dirsCopied} dirs`));
3394
3314
  }
3315
+ if (baseline.baselineExcluded.length > 0) {
3316
+ const onDisk = new Set(baseline.baselineExcludedOnDisk);
3317
+ const names = baseline.baselineExcluded.map((t) => {
3318
+ const name = t.replace(/^\.claude\//, "");
3319
+ return onDisk.has(t) ? `${name} (still on disk)` : name;
3320
+ });
3321
+ log(
3322
+ assetRow(
3323
+ "skip",
3324
+ "excluded by you",
3325
+ onDisk.size > 0 ? `${names.length} \u2014 ${names.join(", ")} \xB7 "still on disk" = \uC774\uC804 \uC124\uCE58\uBCF8\uC774\uB77C \uC9C0\uC6B0\uC9C0 \uC54A\uB294\uB2E4 (\uC81C\uAC70: agent-harness uninstall)` : `${names.length} \u2014 ${names.join(", ")}`
3326
+ )
3327
+ );
3328
+ }
3329
+ if (baseline.superseded.removed.length > 0) {
3330
+ log(
3331
+ assetRow(
3332
+ "success",
3333
+ "superseded \u2014 cleaned",
3334
+ `${baseline.superseded.removed.length} \u2014 ${baseline.superseded.removed.map((t) => t.replace(/^\.claude\//, "")).join(", ")} \xB7 \uB418\uB3CC\uB9AC\uB824\uBA74 \uADF8 \uC790\uC0B0 \uC5C6\uC774 \uC7AC\uC124\uCE58`
3335
+ )
3336
+ );
3337
+ }
3338
+ if (baseline.superseded.kept.length > 0) {
3339
+ log(
3340
+ assetRow(
3341
+ "skip",
3342
+ "superseded \u2014 kept",
3343
+ `${baseline.superseded.kept.length} \u2014 ${baseline.superseded.kept.map((t) => t.replace(/^\.claude\//, "")).join(", ")} \xB7 \uC774\uBC88 \uC120\uD0DD\uC774 \uB300\uCCB4\uD588\uC9C0\uB9CC \uC9C0\uC6B0\uC9C0 \uC54A\uC558\uB2E4 (\uC81C\uAC70: agent-harness uninstall)`
3344
+ )
3345
+ );
3346
+ }
3395
3347
  const TEMPLATES_COL = 28;
3396
3348
  if (baseline.rootClaudeMd) {
3397
3349
  const n = baseline.rootClaudeMd.tracks.length;
@@ -3427,16 +3379,6 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
3427
3379
  }
3428
3380
  const mcpList = baseline.mcpServers.join(", ") || "(none)";
3429
3381
  log(assetRow("success", ".mcp.json", mcpList, TEMPLATES_COL));
3430
- if (baseline.envFiles.mcpAllowlist) {
3431
- log(
3432
- assetRow(
3433
- "success",
3434
- ".mcp-allowlist",
3435
- `${baseline.envFiles.mcpAllowlist.length} servers (D35 opt-in gate)`,
3436
- TEMPLATES_COL
3437
- )
3438
- );
3439
- }
3440
3382
  if (!withEcc && baseline.categories) {
3441
3383
  log("");
3442
3384
  log(
@@ -3548,15 +3490,26 @@ function installAction(options, deps = {}) {
3548
3490
  }
3549
3491
  const forceInclude = normalizeRepeatable(options.with);
3550
3492
  const forceExclude = normalizeRepeatable(options.without);
3493
+ const baselineIds = new Set(
3494
+ listBaselineTargets({ tracks: options.track ?? [] }).map((t) => t.id)
3495
+ );
3496
+ const baselineExclude = forceExclude.filter((id) => baselineIds.has(id));
3551
3497
  const validIds = new Set(EXTERNAL_ASSETS.map((a) => a.id));
3552
- for (const id of [...forceInclude, ...forceExclude]) {
3553
- if (!validIds.has(id)) {
3554
- err(
3555
- c.yellow(
3556
- `[WARN] Unknown asset id '${id}' (--with/--without). Skipping. Use one of: ${[...validIds].sort().join(", ")}`
3557
- )
3558
- );
3559
- }
3498
+ for (const id of forceInclude) {
3499
+ if (validIds.has(id)) continue;
3500
+ err(
3501
+ c.yellow(
3502
+ id.startsWith(BASELINE_PREFIX) ? `[WARN] '${id}' cannot be used with --with \u2014 track baseline assets install by default. Use --without to drop one.` : `[WARN] Unknown asset id '${id}' (--with). Skipping. Use one of: ${[...validIds].sort().join(", ")}`
3503
+ )
3504
+ );
3505
+ }
3506
+ for (const id of forceExclude) {
3507
+ if (validIds.has(id) || baselineIds.has(id)) continue;
3508
+ err(
3509
+ c.yellow(
3510
+ id.startsWith(BASELINE_PREFIX) ? `[WARN] '${id}' is not installed by the selected track(s) \u2014 nothing to exclude. Available: ${[...baselineIds].sort().join(", ")}` : `[WARN] Unknown asset id '${id}' (--without). Skipping. Use one of: ${[...validIds].sort().join(", ")}`
3511
+ )
3512
+ );
3560
3513
  }
3561
3514
  const filteredInclude = forceInclude.filter((id) => validIds.has(id));
3562
3515
  const filteredExclude = forceExclude.filter((id) => validIds.has(id));
@@ -3564,6 +3517,7 @@ function installAction(options, deps = {}) {
3564
3517
  const spec = {
3565
3518
  tracks: options.track ?? [],
3566
3519
  ...userOverride ? { userOverride } : {},
3520
+ ...baselineExclude.length > 0 ? { baselineExclude } : {},
3567
3521
  // v26.81.0 (ADR-022, BREAKING) — 자산 1:1 boolean 13종 삭제. 자산 선택은 위
3568
3522
  // userOverride(--with <id>)로 일원화. 잔존 = 설치 동작 옵션만.
3569
3523
  options: {
@@ -3761,7 +3715,7 @@ function registerListCommand(cli2) {
3761
3715
  // src/commands/uninstall.ts
3762
3716
  init_esm_shims();
3763
3717
  import { spawnSync as spawnSync2 } from "child_process";
3764
- import { existsSync as existsSync15, readFileSync as readFileSync14, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "fs";
3718
+ import { existsSync as existsSync15, readFileSync as readFileSync14, rmSync, writeFileSync as writeFileSync7 } from "fs";
3765
3719
  import { join as join13, resolve as resolve4 } from "path";
3766
3720
 
3767
3721
  // src/uninstall-interactive.ts
@@ -5163,7 +5117,7 @@ function stripRootImport(projectDir) {
5163
5117
  const stripped = current === null ? null : stripHarnessImport(current);
5164
5118
  if (stripped === null) return false;
5165
5119
  try {
5166
- writeFileSync8(join13(projectDir, "CLAUDE.md"), stripped, "utf8");
5120
+ writeFileSync7(join13(projectDir, "CLAUDE.md"), stripped, "utf8");
5167
5121
  return true;
5168
5122
  } catch {
5169
5123
  return false;
@@ -5200,7 +5154,7 @@ function defaultSpawn2(cmd, args) {
5200
5154
  }
5201
5155
  function defaultRm(path) {
5202
5156
  if (existsSync15(path)) {
5203
- rmSync2(path, { recursive: true, force: true });
5157
+ rmSync(path, { recursive: true, force: true });
5204
5158
  }
5205
5159
  }
5206
5160
  function registerUninstallCommand(cli2) {
@@ -5428,6 +5382,18 @@ var CLI_BASE_LABELS = {
5428
5382
  antigravity: "Antigravity (Google)"
5429
5383
  };
5430
5384
  var INSTALL_TARGET_PAGES = [
5385
+ // 2026-08-16 — 트랙이 고르는 자산을 **맨 앞에서** 전부 보여준다. 그전까지 이 항목들은 화면에
5386
+ // 한 번도 안 나온 채 깔렸고, 사용자는 설치가 끝난 뒤 요약에서 처음 봤다.
5387
+ //
5388
+ // 2페이지로 나눈 근거는 실측이다: 최대 트랙(`full`)이 32항목(rules 6 · agents 9 · hooks 3 ·
5389
+ // skills 14)이라 clack groupMultiselect 의 "페이지당 ≤ ~30행" 제약을 한 페이지로는 못 지킨다.
5390
+ // 가르는 축은 역할이다 — 정책·가드가 한쪽, 실행 능력이 다른 쪽.
5391
+ { label: "Track baseline \u2014 Rules & Hooks (\uC815\uCC45\xB7\uAC00\uB4DC)", cats: [], baseline: ["rules", "hooks"] },
5392
+ {
5393
+ label: "Track baseline \u2014 Agents & Skills (\uC5D0\uC774\uC804\uD2B8\xB7\uC2A4\uD0AC)",
5394
+ cats: [],
5395
+ baseline: ["agents", "skills"]
5396
+ },
5431
5397
  { label: "Dev Core (Frontend \xB7 Backend \xB7 Data)", cats: ["frontend", "backend", "data"] },
5432
5398
  { label: "Dev Tools (Security \xB7 Quality \xB7 Understanding)", cats: ["dev-tools", "understanding"] },
5433
5399
  { label: "Business (PM \xB7 Executive \xB7 Documents)", cats: ["business"] },
@@ -5448,10 +5414,25 @@ function expandDevMethodBundle(ids) {
5448
5414
  const rest = ids.filter((v2) => v2 !== DEV_METHOD_BUNDLE_VALUE);
5449
5415
  return ids.includes(DEV_METHOD_BUNDLE_VALUE) ? [...rest, ...bundleMemberValues()] : rest;
5450
5416
  }
5451
- function buildPageGroups(cats, initialSet, installedSet = /* @__PURE__ */ new Set()) {
5417
+ var BASELINE_TITLES = {
5418
+ rules: "Rules \u2014 \uC0C1\uC2DC \uC0C1\uC8FC\uD558\uB294 \uB300\uC6D0\uCE59 (4 CLI \uC804\uBD80\uC5D0 \uC124\uCE58)",
5419
+ hooks: "Hooks \u2014 \uACB0\uC815\uB860\uC801 \uAC00\uB4DC (\uD574\uC81C\uD558\uBA74 settings.json \uBC30\uC120\uB3C4 \uD568\uAED8 \uBE60\uC9C4\uB2E4)",
5420
+ agents: "Agents \u2014 \uB3C5\uB9BD \uB808\uC778 (\uB9AC\uBDF0\xB7\uAC80\uC99D)",
5421
+ skills: "Skills \u2014 \uD544\uC694\uD560 \uB54C \uC5F4\uB9AC\uB294 \uC791\uC5C5 \uC808\uCC28"
5422
+ };
5423
+ function buildPageGroups(cats, initialSet, installedSet = /* @__PURE__ */ new Set(), baselineKinds = [], baselineTargets = []) {
5452
5424
  const installedMark = (value) => installedSet.has(value) ? " \u25CF installed" : "";
5453
5425
  const groups = {};
5454
5426
  const flatItems = [];
5427
+ for (const kind of baselineKinds) {
5428
+ const items = baselineTargets.filter((t) => t.kind === kind).map((t) => ({
5429
+ value: t.id,
5430
+ label: ` ${t.name}${installedMark(t.id)}`
5431
+ }));
5432
+ if (items.length === 0) continue;
5433
+ groups[BASELINE_TITLES[kind]] = items;
5434
+ flatItems.push(...items);
5435
+ }
5455
5436
  for (const cat of cats) {
5456
5437
  const items = [];
5457
5438
  for (const o of VISIBLE_OPTION_DEFS.filter((d2) => d2.category === cat)) {
@@ -5586,8 +5567,26 @@ Proceed?`,
5586
5567
  });
5587
5568
  return q(result) ? null : result;
5588
5569
  },
5570
+ confirmSupersededCleanup: async (items, context) => {
5571
+ const names = items.map((i) => i.target.replace(/^\.claude\//, "")).join("\n ");
5572
+ const result = await ue({
5573
+ message: [
5574
+ `${context.by.join(" \xB7 ")} \uC744(\uB97C) \uACE8\uB77C\uC11C \uC544\uB798 ${items.length}\uAC1C\uAC00 \uC124\uCE58 \uB300\uC0C1\uC5D0\uC11C \uBE60\uC84C\uC2B5\uB2C8\uB2E4.`,
5575
+ "\uADF8\uB7F0\uB370 \uC774\uC804 \uC124\uCE58\uBCF8\uC774 \uB514\uC2A4\uD06C\uC5D0 \uADF8\uB300\uB85C \uB0A8\uC544 \uC788\uC2B5\uB2C8\uB2E4:",
5576
+ ` ${names}`,
5577
+ "",
5578
+ `\uADF8\uB300\uB85C \uB450\uBA74 \uAC19\uC740 \uC77C\uC744 \uD558\uB294 \uC5D0\uC774\uC804\uD2B8\uAC00 \uB450 \uBC8C\uC774\uACE0, \uB9E4 \uC138\uC158 ~${context.tokens} tok \uB97C \uACC4\uC18D \uBB45\uB2C8\uB2E4.`,
5579
+ "(\uD558\uB124\uC2A4\uAC00 \uAE54\uC558\uACE0 \uC218\uC815\uB418\uC9C0 \uC54A\uC740 \uD30C\uC77C\uB9CC \uB300\uC0C1\uC785\uB2C8\uB2E4 \u2014 \uC9C1\uC811 \uACE0\uCE5C \uD30C\uC77C\uC740 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4)",
5580
+ "",
5581
+ "\uC815\uB9AC\uD560\uAE4C\uC694?"
5582
+ ].join("\n"),
5583
+ initialValue: true
5584
+ });
5585
+ return q(result) ? null : result;
5586
+ },
5589
5587
  selectInstallTargets: async (initialChecked, step, recap) => {
5590
5588
  const pages = INSTALL_TARGET_PAGES;
5589
+ const baselineTargets = listBaselineTargets({ tracks: recap?.tracks ?? [] });
5591
5590
  const displayInitial = collapseDevMethodBundle(initialChecked);
5592
5591
  const initialSet = new Set(displayInitial);
5593
5592
  const collected = new Set(displayInitial);
@@ -5600,10 +5599,25 @@ Proceed?`,
5600
5599
  let aborted = false;
5601
5600
  try {
5602
5601
  let pageIdx = 0;
5602
+ let dir = 1;
5603
5603
  while (pageIdx < pages.length) {
5604
5604
  const page = pages[pageIdx];
5605
5605
  if (!page) break;
5606
- const { groups, flatItems } = buildPageGroups(page.cats, initialSet, installedSet);
5606
+ const { groups, flatItems } = buildPageGroups(
5607
+ page.cats,
5608
+ initialSet,
5609
+ installedSet,
5610
+ page.baseline ?? [],
5611
+ baselineTargets
5612
+ );
5613
+ if (flatItems.length === 0) {
5614
+ pageIdx += dir;
5615
+ if (pageIdx < 0) {
5616
+ aborted = true;
5617
+ break;
5618
+ }
5619
+ continue;
5620
+ }
5607
5621
  const selectedNow = flatItems.filter((it3) => collected.has(it3.value)).map((it3) => it3.value);
5608
5622
  const pageDefault = flatItems.filter((it3) => initialSet.has(it3.value)).length;
5609
5623
  const totalSelected = collected.size;
@@ -5629,11 +5643,13 @@ Proceed?`,
5629
5643
  aborted = true;
5630
5644
  break;
5631
5645
  }
5646
+ dir = -1;
5632
5647
  pageIdx--;
5633
5648
  continue;
5634
5649
  }
5635
5650
  for (const it3 of flatItems) collected.delete(it3.value);
5636
5651
  for (const v2 of result) collected.add(v2);
5652
+ dir = 1;
5637
5653
  pageIdx++;
5638
5654
  }
5639
5655
  if (!aborted) {
@@ -5657,14 +5673,21 @@ Proceed?`,
5657
5673
  function splitInstallTargets(targets) {
5658
5674
  const optionKeys = [];
5659
5675
  const assetIds = [];
5676
+ const baselineIds = [];
5660
5677
  for (const t of targets) {
5661
5678
  if (t.startsWith("option:")) {
5662
5679
  optionKeys.push(t.slice("option:".length));
5663
5680
  } else if (t.startsWith("asset:")) {
5664
5681
  assetIds.push(t.slice("asset:".length));
5682
+ } else if (t.startsWith(BASELINE_PREFIX)) {
5683
+ baselineIds.push(t);
5665
5684
  }
5666
5685
  }
5667
- return { optionKeys, assetIds };
5686
+ return { optionKeys, assetIds, baselineIds };
5687
+ }
5688
+ function baselineExcludeFrom(offered, checked) {
5689
+ const keep = new Set(checked);
5690
+ return offered.filter((t) => !keep.has(t.id)).map((t) => t.id);
5668
5691
  }
5669
5692
  function toOptionFlags(keys) {
5670
5693
  const picked = new Set(keys);
@@ -5684,7 +5707,9 @@ function installedTargetState(projectDir) {
5684
5707
  function initialTargetSelection(tracks, installedProjectAssetIds) {
5685
5708
  const ids = new Set(recommendedExternalAssets(tracks));
5686
5709
  for (const id of installedProjectAssetIds) ids.add(id);
5687
- return [...ids].map((id) => `asset:${id}`);
5710
+ const assets = [...ids].map((id) => `asset:${id}`);
5711
+ const baseline = listBaselineTargets({ tracks }).map((t) => t.id);
5712
+ return [...assets, ...baseline];
5688
5713
  }
5689
5714
  async function runInteractive(projectDir, deps = {}) {
5690
5715
  const prompts = deps.prompts ?? defaultPrompts;
@@ -5786,16 +5811,21 @@ ${formatSummary(spec)}`
5786
5811
  } else {
5787
5812
  const finalTracks = tracks;
5788
5813
  const finalCli = cli2;
5789
- const { optionKeys, assetIds } = splitInstallTargets(targetSelections ?? []);
5814
+ const { optionKeys, assetIds, baselineIds } = splitInstallTargets(targetSelections ?? []);
5790
5815
  const options = toOptionFlags(optionKeys);
5791
5816
  const userOverride = targetSelections === null ? void 0 : computeUserOverride(finalTracks, assetIds);
5817
+ const baselineExclude = targetSelections === null ? [] : baselineExcludeFrom(listBaselineTargets({ tracks: finalTracks }), baselineIds);
5792
5818
  const scopeLabel = scope === "global" ? "Global (writes to ~/.claude/, ~/.codex/, npm -g)" : "Project (current directory only)";
5793
5819
  const summary = `${formatSummary({
5794
5820
  tracks: finalTracks,
5795
5821
  options,
5796
5822
  cli: finalCli,
5797
5823
  projectDir,
5798
- ...userOverride ? { userOverride } : {}
5824
+ ...userOverride ? { userOverride } : {},
5825
+ // 해제 목록을 안 넘기면 이 화면이 **제외 유무와 문자열이 완전히 같아진다** — 같은 화면이
5826
+ // 외부 자산 제거는 `-User removed:` 로 이미 보고하므로, 없음은 "아무것도 안 빠졌다"로
5827
+ // 읽힌다. 상주 비용을 줄이려고 20개를 푼 사용자가 그대로인 숫자를 보게 된다.
5828
+ ...baselineExclude.length > 0 ? { baselineExclude } : {}
5799
5829
  })}
5800
5830
  SCOPE ${scopeLabel}`;
5801
5831
  const confirmed = await prompts.confirmInstall(
@@ -5810,22 +5840,59 @@ ${summary}`
5810
5840
  prompts.outro("Cancelled by user.");
5811
5841
  return { ok: false, reason: "cancelled" };
5812
5842
  }
5843
+ const spec = {
5844
+ tracks: finalTracks,
5845
+ options,
5846
+ cli: finalCli,
5847
+ projectDir,
5848
+ scope,
5849
+ ...userOverride ? { userOverride } : {},
5850
+ ...baselineExclude.length > 0 ? { baselineExclude } : {}
5851
+ };
5852
+ const superseded = findSuperseded(
5853
+ projectDir,
5854
+ buildManifestSpec(spec),
5855
+ readInstallLog(projectDir)
5856
+ );
5857
+ let cleanSuperseded = false;
5858
+ if (superseded.length > 0) {
5859
+ const cause = supersededCause(projectDir, spec, readInstallLog(projectDir), superseded);
5860
+ const answer = await prompts.confirmSupersededCleanup(superseded, {
5861
+ by: cause.length > 0 ? cause : ["\uC774\uBC88 \uC120\uD0DD"],
5862
+ tokens: supersededTokens(superseded)
5863
+ });
5864
+ if (answer === null) {
5865
+ step = "confirm";
5866
+ continue;
5867
+ }
5868
+ cleanSuperseded = answer;
5869
+ }
5813
5870
  prompts.outro(stepLabel(WIZARD.INSTALL, "Installing..."));
5814
5871
  return {
5815
5872
  ok: true,
5816
5873
  mode,
5817
- spec: {
5818
- tracks: finalTracks,
5819
- options,
5820
- cli: finalCli,
5821
- projectDir,
5822
- scope,
5823
- ...userOverride ? { userOverride } : {}
5824
- }
5874
+ spec: { ...spec, ...cleanSuperseded ? { cleanSuperseded } : {} }
5825
5875
  };
5826
5876
  }
5827
5877
  }
5828
5878
  }
5879
+ function supersededTokens(items) {
5880
+ const r = residentCost(items.map((i) => ({ source: i.source, target: i.target })));
5881
+ return r.rules + r.agentDescriptors + r.skillDescriptors;
5882
+ }
5883
+ function supersededCause(projectDir, spec, log, found) {
5884
+ const selected = spec.userOverride?.forceInclude ?? [];
5885
+ return selected.filter((id) => {
5886
+ const without = {
5887
+ ...spec,
5888
+ userOverride: {
5889
+ forceInclude: selected.filter((x) => x !== id),
5890
+ forceExclude: [...spec.userOverride?.forceExclude ?? [], id]
5891
+ }
5892
+ };
5893
+ return findSuperseded(projectDir, buildManifestSpec(without), log).length < found.length;
5894
+ });
5895
+ }
5829
5896
  function tracksEqual(a, b2) {
5830
5897
  if (a.length !== b2.length) return false;
5831
5898
  const sortedA = [...a].sort();
@@ -5841,6 +5908,7 @@ function computeUserOverride(tracks, assetIds) {
5841
5908
  return { forceInclude, forceExclude };
5842
5909
  }
5843
5910
  function formatSummary(spec) {
5911
+ const baselineExcluded = new Set(spec.baselineExclude ?? []);
5844
5912
  const opts = Object.keys(spec.options).filter((k2) => spec.options[k2]).map((k2) => k2.replace(/^with/, "").toLowerCase());
5845
5913
  const optsLabel = opts.length > 0 ? opts.join(", ") : "(none added)";
5846
5914
  const lines = [
@@ -5862,7 +5930,11 @@ function formatSummary(spec) {
5862
5930
  lines.push(` \xB7 ${cat}: ${ids.join(", ")}`);
5863
5931
  }
5864
5932
  const cost = formatResidentCostLine(
5865
- residentCost(buildManifest(spec).filter((e2) => e2.applies(spec))),
5933
+ residentCost(
5934
+ buildManifest(spec).filter(
5935
+ (e2) => e2.applies(spec) && !isBaselineExcluded(e2.target, baselineExcluded)
5936
+ )
5937
+ ),
5866
5938
  summarizeContextCost(finalAssets).unmeasuredCount
5867
5939
  );
5868
5940
  if (cost) lines.push(` \xB7 ${cost}`);
@@ -5875,6 +5947,11 @@ function formatSummary(spec) {
5875
5947
  lines.push(` -User removed: ${spec.userOverride.forceExclude.join(", ")}`);
5876
5948
  }
5877
5949
  }
5950
+ if (baselineExcluded.size > 0) {
5951
+ lines.push(
5952
+ ` -Excluded by you: ${baselineExcluded.size} \u2014 ${[...baselineExcluded].map((id) => id.replace(BASELINE_PREFIX, "")).join(", ")}`
5953
+ );
5954
+ }
5878
5955
  return lines.join("\n");
5879
5956
  }
5880
5957