@uzysjung/agent-harness 26.146.1 → 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 +604 -529
  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 +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.1",
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: {
@@ -778,6 +802,46 @@ init_esm_shims();
778
802
  import { resolve as resolve2 } from "path";
779
803
  import { fileURLToPath } from "url";
780
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
+
781
845
  // src/cli-targets.ts
782
846
  init_esm_shims();
783
847
  var CLI_BASE_SORT_ORDER = {
@@ -915,127 +979,18 @@ init_esm_shims();
915
979
  import {
916
980
  chmodSync as chmodSync2,
917
981
  existsSync as existsSync13,
918
- mkdirSync as mkdirSync6,
919
- readdirSync as readdirSync4,
982
+ mkdirSync as mkdirSync4,
983
+ readdirSync as readdirSync3,
920
984
  readFileSync as readFileSync12,
921
- writeFileSync as writeFileSync7
985
+ unlinkSync as unlinkSync2,
986
+ writeFileSync as writeFileSync6
922
987
  } from "fs";
923
- 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";
924
989
 
925
990
  // src/ci-scaffold.ts
926
991
  init_esm_shims();
927
- import { existsSync as existsSync2 } from "fs";
928
- import { join as join2 } from "path";
929
-
930
- // src/fs-ops.ts
931
- init_esm_shims();
932
- import {
933
- copyFileSync,
934
- cpSync,
935
- existsSync,
936
- constants as fsConstants,
937
- mkdirSync,
938
- readdirSync,
939
- readFileSync,
940
- realpathSync,
941
- renameSync,
942
- rmdirSync
943
- } from "fs";
944
- import { dirname, join } from "path";
945
- function ensureDir(path) {
946
- mkdirSync(path, { recursive: true });
947
- }
948
- function copyFile(source, target) {
949
- if (!existsSync(source)) {
950
- throw new Error(`Source not found: ${source}`);
951
- }
952
- mkdirSync(dirname(target), { recursive: true });
953
- copyFileSync(source, target);
954
- }
955
- function copyDir(source, target) {
956
- if (!existsSync(source)) {
957
- throw new Error(`Source dir not found: ${source}`);
958
- }
959
- mkdirSync(target, { recursive: true });
960
- cpSync(source, target, { recursive: true, force: true });
961
- }
962
- var MAX_BACKUP_CLAIMS = 100;
963
- function claimBackupPath(base, claim) {
964
- for (let n = 1; n <= MAX_BACKUP_CLAIMS; n++) {
965
- const candidate = n === 1 ? base : `${base}-${n}`;
966
- try {
967
- claim(candidate);
968
- return candidate;
969
- } catch (err) {
970
- if (err.code !== "EEXIST") throw err;
971
- }
972
- }
973
- throw new Error(`Cannot allocate a backup path for ${base} (${MAX_BACKUP_CLAIMS} taken)`);
974
- }
975
- function backupDir(target, now = /* @__PURE__ */ new Date()) {
976
- if (!existsSync(target)) {
977
- return null;
978
- }
979
- const backup = claimBackupPath(`${target}.backup-${formatStamp(now)}`, (c3) => mkdirSync(c3));
980
- rmdirSync(backup);
981
- renameSync(target, backup);
982
- return backup;
983
- }
984
- function copyBackupDir(target, now = /* @__PURE__ */ new Date()) {
985
- if (!existsSync(target)) {
986
- return null;
987
- }
988
- const backup = claimBackupPath(`${target}.backup-${formatStamp(now)}`, (c3) => mkdirSync(c3));
989
- cpSync(realpathSync(target), backup, { recursive: true });
990
- return backup;
991
- }
992
- function backupFileIfChanged(target, newContent, now = /* @__PURE__ */ new Date()) {
993
- if (!existsSync(target)) {
994
- return null;
995
- }
996
- if (readFileSync(target, "utf-8") === newContent) {
997
- return null;
998
- }
999
- return backupFile(target, now);
1000
- }
1001
- function backupFile(target, now = /* @__PURE__ */ new Date()) {
1002
- return claimBackupPath(
1003
- `${target}.backup-${formatStamp(now)}`,
1004
- (c3) => copyFileSync(target, c3, fsConstants.COPYFILE_EXCL)
1005
- );
1006
- }
1007
- function listFilesRecursive(dir, prefix = "") {
1008
- if (!existsSync(dir)) return [];
1009
- const out = [];
1010
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1011
- const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
1012
- if (entry.isDirectory()) {
1013
- out.push(...listFilesRecursive(join(dir, entry.name), rel));
1014
- } else if (entry.isFile()) {
1015
- out.push(rel);
1016
- }
1017
- }
1018
- return out;
1019
- }
1020
- function formatStamp(now) {
1021
- return now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z").slice(0, 15);
1022
- }
1023
- function ensureProjectSkeleton(projectDir) {
1024
- const dirs = [
1025
- ".claude/commands/uzys",
1026
- ".claude/commands/ecc",
1027
- ".claude/rules",
1028
- ".claude/skills",
1029
- ".claude/agents",
1030
- ".claude/hooks",
1031
- "docs/decisions"
1032
- ];
1033
- for (const d2 of dirs) {
1034
- mkdirSync(join(projectDir, d2), { recursive: true });
1035
- }
1036
- }
1037
-
1038
- // src/ci-scaffold.ts
992
+ import { existsSync } from "fs";
993
+ import { join } from "path";
1039
994
  var PYTHON_TRACKS = "data|csr-fastapi|full";
1040
995
  var VARIANTS = [
1041
996
  {
@@ -1059,12 +1014,12 @@ function installCiScaffold(ctx) {
1059
1014
  for (const v2 of VARIANTS) {
1060
1015
  if (!v2.applies(ctx.tracks)) continue;
1061
1016
  const relTarget = `.github/workflows/${v2.target}`;
1062
- const target = join2(ctx.projectDir, relTarget);
1063
- if (existsSync2(target)) {
1017
+ const target = join(ctx.projectDir, relTarget);
1018
+ if (existsSync(target)) {
1064
1019
  report.skippedExisting.push(relTarget);
1065
1020
  continue;
1066
1021
  }
1067
- copyFile(join2(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
1022
+ copyFile(join(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
1068
1023
  report.written.push(relTarget);
1069
1024
  }
1070
1025
  return report;
@@ -1075,8 +1030,8 @@ init_esm_shims();
1075
1030
 
1076
1031
  // src/antigravity/transform.ts
1077
1032
  init_esm_shims();
1078
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1079
- 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";
1080
1035
 
1081
1036
  // src/codex/agents-md.ts
1082
1037
  init_esm_shims();
@@ -1085,7 +1040,7 @@ function renameSlashes(text) {
1085
1040
  }
1086
1041
  function renderAgentsMd(params) {
1087
1042
  const body = params.claudeMd.replace(/^#\s+.*\r?\n/, "").trim();
1088
- 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 ?? "");
1089
1044
  return renameSlashes(replaced);
1090
1045
  }
1091
1046
 
@@ -1121,204 +1076,8 @@ function portBody(body) {
1121
1076
 
1122
1077
  // src/owned-write.ts
1123
1078
  init_esm_shims();
1124
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1125
- import { dirname as dirname3, relative, sep } from "path";
1126
-
1127
- // src/install-log.ts
1128
- init_esm_shims();
1129
- import { createHash } from "crypto";
1130
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
1131
- import { dirname as dirname2, join as join3 } from "path";
1132
- var INSTALL_LOG_FILENAME = ".harness-install.json";
1133
- var INSTALL_LOG_DIR = ".uzys-agent-harness";
1134
- var LEGACY_INSTALL_LOG_DIR = ".claude";
1135
- var INSTALL_LOG_VERSION = 1;
1136
- var POLICY_DIRS = [
1137
- { dir: "rules", ext: ".md" },
1138
- { dir: "agents", ext: ".md" },
1139
- { dir: "commands/uzys", ext: ".md" },
1140
- { dir: "hooks", ext: ".sh" }
1141
- ];
1142
- function buildAssetEntries(report, scope) {
1143
- if (!report) return [];
1144
- return report.attempted.filter((r) => r.ok).map((r) => assetToLogEntry(r.asset, scope, r.version));
1145
- }
1146
- function assetToLogEntry(asset, scope, version) {
1147
- const detail = methodDetail(asset.method);
1148
- const entry = {
1149
- id: asset.id,
1150
- category: asset.category,
1151
- method: asset.method.kind,
1152
- scope,
1153
- detail
1154
- };
1155
- if (version) entry.version = version;
1156
- return entry;
1157
- }
1158
- function methodDetail(method) {
1159
- switch (method.kind) {
1160
- case "plugin":
1161
- return { marketplace: method.marketplace, pluginId: method.pluginId };
1162
- case "skill":
1163
- return { source: method.source, ...method.skill ? { skill: method.skill } : {} };
1164
- case "npm":
1165
- return { pkg: method.pkg };
1166
- case "npx-run":
1167
- return { cmd: method.cmd, args: (method.args ?? []).join(" ") };
1168
- case "shell-script":
1169
- return { script: method.script, args: method.args.join(" ") };
1170
- case "internal":
1171
- return { key: method.key };
1172
- }
1173
- }
1174
- function buildInstallLog(spec, external, scope, rootClaudeMd, previous, claudeDirMovedAside = false, rootFiles = []) {
1175
- const templates = {
1176
- claudeDir: ".claude/",
1177
- ...spec.cli.includes("codex") ? { codexDir: ".codex/" } : {},
1178
- ...spec.cli.includes("opencode") ? { opencodeDir: ".opencode/" } : {},
1179
- ...rootClaudeMd ? { rootClaudeMd } : {}
1180
- };
1181
- const log = {
1182
- schemaVersion: INSTALL_LOG_VERSION,
1183
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1184
- scope,
1185
- spec: {
1186
- tracks: spec.tracks,
1187
- cli: spec.cli
1188
- },
1189
- // 이번 설치가 만든 항목이 이기고, 이번에 안 만든 항목은 이전 값을 그대로 둔다.
1190
- // (예: claude 로 깔고 나중에 codex 만 추가 설치해도 root CLAUDE.md 기록이 살아남는다)
1191
- templates: { ...previous?.templates, ...templates },
1192
- assets: mergeAssets(
1193
- claudeDirMovedAside ? previous?.assets?.filter(survivesClaudeDirRename) : previous?.assets,
1194
- buildAssetEntries(external, scope)
1195
- )
1196
- };
1197
- const mergedRootFiles = mergeRootFiles(previous?.rootFiles, rootFiles);
1198
- if (mergedRootFiles.length > 0) log.rootFiles = mergedRootFiles;
1199
- return log;
1200
- }
1201
- function mergeRootFiles(previous, current) {
1202
- const byPath = /* @__PURE__ */ new Map();
1203
- for (const file of [...previous ?? [], ...current]) {
1204
- const prior = byPath.get(file.path);
1205
- byPath.set(
1206
- file.path,
1207
- prior ? {
1208
- path: file.path,
1209
- change: prior.change === "created" ? "created" : file.change,
1210
- notes: [.../* @__PURE__ */ new Set([...prior.notes, ...file.notes])]
1211
- } : file
1212
- );
1213
- }
1214
- return [...byPath.values()];
1215
- }
1216
- function survivesClaudeDirRename(asset) {
1217
- if (asset.scope === "global") return true;
1218
- switch (asset.method) {
1219
- case "plugin":
1220
- return true;
1221
- // `~/.claude/plugins/cache` — 프로젝트 밖
1222
- case "npm":
1223
- return true;
1224
- // `node_modules/`
1225
- case "skill":
1226
- return false;
1227
- // `npx skills add` project scope → `.claude/skills/`
1228
- case "shell-script":
1229
- return false;
1230
- // ecc-prune → `.claude/local-plugins/`
1231
- case "npx-run":
1232
- return false;
1233
- case "internal":
1234
- return false;
1235
- }
1236
- }
1237
- function mergeAssets(previous, current) {
1238
- if (!previous || previous.length === 0) return [...current];
1239
- const currentById = new Map(current.map((a) => [a.id, a]));
1240
- const previousIds = new Set(previous.map((a) => a.id));
1241
- return [
1242
- ...previous.map((a) => currentById.get(a.id) ?? a),
1243
- ...current.filter((a) => !previousIds.has(a.id))
1244
- ];
1245
- }
1246
- function hashContent(content) {
1247
- return createHash("sha256").update(content, "utf8").digest("hex");
1248
- }
1249
- function isHarnessOwned(baseline, key, current) {
1250
- const recorded = baseline.get(key);
1251
- return recorded !== void 0 && recorded === hashContent(current);
1252
- }
1253
- function mergeExternalFiles(projectDir, previous, current) {
1254
- const byPath = /* @__PURE__ */ new Map();
1255
- for (const file of [...previous ?? [], ...current]) byPath.set(file.path, file);
1256
- return [...byPath.values()].filter((f) => existsSync3(join3(projectDir, f.path)));
1257
- }
1258
- function collectSkillHashes(projectDir) {
1259
- const skillsDir = join3(projectDir, ".claude/skills");
1260
- return listFilesRecursive(skillsDir).map((rel) => ({
1261
- path: rel,
1262
- sha256: hashContent(readFileSync2(join3(skillsDir, rel), "utf8"))
1263
- }));
1264
- }
1265
- function collectPolicyHashes(projectDir, templatesDir) {
1266
- const out = [];
1267
- for (const { dir, ext } of POLICY_DIRS) {
1268
- const targetDir = join3(projectDir, ".claude", dir);
1269
- const sourceDir = join3(templatesDir, dir);
1270
- for (const rel of listFilesRecursive(targetDir)) {
1271
- if (!rel.endsWith(ext)) continue;
1272
- if (!existsSync3(join3(sourceDir, rel))) continue;
1273
- out.push({
1274
- path: `${dir}/${rel}`,
1275
- sha256: hashContent(readFileSync2(join3(targetDir, rel), "utf8"))
1276
- });
1277
- }
1278
- }
1279
- return out;
1280
- }
1281
- function writeInstallLog(projectDir, log) {
1282
- const path = installLogPath(projectDir);
1283
- mkdirSync2(dirname2(path), { recursive: true });
1284
- writeFileSync(path, `${JSON.stringify(log, null, 2)}
1285
- `, "utf8");
1286
- migrateAwayLegacyLog(projectDir);
1287
- return path;
1288
- }
1289
- function migrateAwayLegacyLog(projectDir) {
1290
- const legacy = legacyInstallLogPath(projectDir);
1291
- if (!existsSync3(legacy)) return;
1292
- try {
1293
- rmSync(legacy);
1294
- } catch {
1295
- }
1296
- }
1297
- function readInstallLog(projectDir) {
1298
- const path = [installLogPath(projectDir), legacyInstallLogPath(projectDir)].find(
1299
- (p2) => existsSync3(p2)
1300
- );
1301
- if (!path) return null;
1302
- try {
1303
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
1304
- if (Array.isArray(parsed.assets)) {
1305
- parsed.assets = parsed.assets.map(
1306
- (a) => a.method === "npm-global" ? { ...a, method: "npm" } : a
1307
- );
1308
- }
1309
- return parsed;
1310
- } catch {
1311
- return null;
1312
- }
1313
- }
1314
- function installLogPath(projectDir) {
1315
- return join3(projectDir, INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
1316
- }
1317
- function legacyInstallLogPath(projectDir) {
1318
- return join3(projectDir, LEGACY_INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
1319
- }
1320
-
1321
- // src/owned-write.ts
1079
+ import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
1080
+ import { dirname, relative, sep } from "path";
1322
1081
  function createOwnedWriter(projectDir, baseline, options = {}) {
1323
1082
  const { now = /* @__PURE__ */ new Date(), refreshOnly = false } = options;
1324
1083
  const written = /* @__PURE__ */ new Map();
@@ -1326,22 +1085,22 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
1326
1085
  const backupPaths = [];
1327
1086
  let updated = 0;
1328
1087
  return {
1329
- write(absPath, content) {
1088
+ write(absPath, content, opts) {
1330
1089
  const rel = relative(projectDir, absPath).split(sep).join("/");
1331
1090
  const digest = hashContent(content);
1332
- if (!existsSync4(absPath)) {
1333
- if (refreshOnly) return false;
1334
- mkdirSync3(dirname3(absPath), { recursive: true });
1335
- writeFileSync2(absPath, content);
1091
+ if (!existsSync2(absPath)) {
1092
+ if (refreshOnly && !opts?.createInRefresh) return false;
1093
+ mkdirSync(dirname(absPath), { recursive: true });
1094
+ writeFileSync(absPath, content);
1336
1095
  updated++;
1337
1096
  } else {
1338
- const current = readFileSync3(absPath, "utf8");
1097
+ const current = readFileSync(absPath, "utf8");
1339
1098
  if (current !== content) {
1340
1099
  if (!isHarnessOwned(baseline, rel, current)) {
1341
1100
  backupPaths.push(backupFile(absPath, now));
1342
1101
  backedUp.push(rel);
1343
1102
  }
1344
- writeFileSync2(absPath, content);
1103
+ writeFileSync(absPath, content);
1345
1104
  updated++;
1346
1105
  }
1347
1106
  }
@@ -1359,36 +1118,78 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
1359
1118
  };
1360
1119
  }
1361
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
+
1362
1145
  // src/antigravity/transform.ts
1363
1146
  function runAntigravityTransform(params) {
1364
- const { harnessRoot, projectDir, selectedInternalSkills = [], baseline, refreshOnly } = params;
1147
+ const {
1148
+ harnessRoot,
1149
+ projectDir,
1150
+ selectedInternalSkills = [],
1151
+ rules = [],
1152
+ baseline,
1153
+ refreshOnly
1154
+ } = params;
1365
1155
  const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
1366
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
+ }
1367
1167
  const skillFiles = [];
1368
1168
  for (const id of selectedInternalSkills) {
1369
- const src = join4(harnessRoot, "templates/skills", id, "SKILL.md");
1370
- if (!existsSync5(src)) {
1169
+ const src = join3(harnessRoot, "templates/skills", id, "SKILL.md");
1170
+ if (!existsSync4(src)) {
1371
1171
  continue;
1372
1172
  }
1373
- const target = join4(projectDir, ".agents", "skills", id, "SKILL.md");
1374
- 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;
1375
1175
  skillFiles.push(target);
1376
1176
  }
1377
1177
  return {
1378
1178
  rulesFile,
1179
+ harnessRuleFiles,
1379
1180
  skillFiles,
1380
1181
  ownership: writer.result()
1381
1182
  };
1382
1183
  }
1383
1184
  function writeRules(harnessRoot, projectDir, writer) {
1384
- const claudeMdPath = join4(harnessRoot, "templates/CLAUDE.md");
1385
- const templatePath = join4(harnessRoot, "templates/antigravity/AGENTS.md.template");
1386
- 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)) {
1387
1188
  return null;
1388
1189
  }
1389
- const claudeMd = readFileSync4(claudeMdPath, "utf8");
1390
- const template = readFileSync4(templatePath, "utf8");
1391
- 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");
1392
1193
  const rulesOut = renderAgentsMd({
1393
1194
  template,
1394
1195
  claudeMd,
@@ -1401,18 +1202,18 @@ function writeRules(harnessRoot, projectDir, writer) {
1401
1202
  // src/codex/opt-in.ts
1402
1203
  init_esm_shims();
1403
1204
  import { homedir } from "os";
1404
- import { join as join5 } from "path";
1205
+ import { join as join4 } from "path";
1405
1206
 
1406
1207
  // src/codex/trust-entry.ts
1407
1208
  init_esm_shims();
1408
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1409
- 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";
1410
1211
  var TRUST_BLOCK_REGEX = /\[projects\."([^"]+)"\]/g;
1411
1212
  function registerTrustEntry(opts) {
1412
1213
  const { configPath, projectDir } = opts;
1413
1214
  try {
1414
- mkdirSync4(dirname4(configPath), { recursive: true });
1415
- const existing = existsSync6(configPath) ? readFileSync5(configPath, "utf8") : "";
1215
+ mkdirSync2(dirname2(configPath), { recursive: true });
1216
+ const existing = existsSync5(configPath) ? readFileSync4(configPath, "utf8") : "";
1416
1217
  if (hasTrustEntry(existing, projectDir)) {
1417
1218
  return { status: "already-present" };
1418
1219
  }
@@ -1420,7 +1221,7 @@ function registerTrustEntry(opts) {
1420
1221
  [projects."${projectDir}"]
1421
1222
  trust_level = "trusted"
1422
1223
  `;
1423
- writeFileSync3(configPath, existing + block);
1224
+ writeFileSync2(configPath, existing + block);
1424
1225
  return { status: "registered" };
1425
1226
  } catch (e2) {
1426
1227
  return {
@@ -1436,8 +1237,8 @@ function hasTrustEntry(configContent, projectDir) {
1436
1237
 
1437
1238
  // src/codex/opt-in.ts
1438
1239
  function runCodexOptIn(ctx) {
1439
- const codexHome = ctx.codexHome ?? join5(homedir(), ".codex");
1440
- const configPath = join5(codexHome, "config.toml");
1240
+ const codexHome = ctx.codexHome ?? join4(homedir(), ".codex");
1241
+ const configPath = join4(codexHome, "config.toml");
1441
1242
  const result = registerTrustEntry({ configPath, projectDir: ctx.projectDir });
1442
1243
  return {
1443
1244
  trustEntry: {
@@ -1450,8 +1251,8 @@ function runCodexOptIn(ctx) {
1450
1251
 
1451
1252
  // src/codex/transform.ts
1452
1253
  init_esm_shims();
1453
- import { chmodSync, existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1454
- 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";
1455
1256
 
1456
1257
  // src/codex/config-toml.ts
1457
1258
  init_esm_shims();
@@ -1519,23 +1320,32 @@ function quoteIfNeeded(name) {
1519
1320
  var HOOK_NAMES = ["session-start"];
1520
1321
  var ENV_VAR_RENAME = /CLAUDE_PROJECT_DIR/g;
1521
1322
  function runCodexTransform(params) {
1522
- const { harnessRoot, projectDir, selectedInternalSkills = [], baseline, refreshOnly } = params;
1323
+ const {
1324
+ harnessRoot,
1325
+ projectDir,
1326
+ selectedInternalSkills = [],
1327
+ rules = [],
1328
+ baseline,
1329
+ refreshOnly
1330
+ } = params;
1523
1331
  const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
1524
- const claudeMd = readRequired(join6(harnessRoot, "templates/CLAUDE.md"));
1525
- const agentsTemplate = readRequired(join6(harnessRoot, "templates/codex/AGENTS.md.template"));
1526
- 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"));
1527
1335
  const projectName = basename2(projectDir);
1528
- const mcp = readOptionalJson(join6(harnessRoot, ".mcp.json"));
1529
- const agentsMdPath = join6(projectDir, "AGENTS.md");
1336
+ const mcp = readOptionalJson(join5(harnessRoot, ".mcp.json"));
1337
+ const agentsMdPath = join5(projectDir, "AGENTS.md");
1530
1338
  ensureDir(projectDir);
1531
1339
  const agentsMdOut = renderAgentsMd({
1532
1340
  template: agentsTemplate,
1533
1341
  claudeMd,
1534
1342
  projectName,
1535
- projectContext: renderFillScaffold()
1343
+ projectContext: renderFillScaffold(),
1344
+ // Codex 는 룰 디렉터리가 없다 — 룰이 AGENTS.md 본문에 들어가야 도달한다(§Harness Rules).
1345
+ harnessRules: renderRulesBlock(portRules(harnessRoot, rules))
1536
1346
  });
1537
1347
  writer.write(agentsMdPath, agentsMdOut);
1538
- const configTomlPath = join6(projectDir, ".codex/config.toml");
1348
+ const configTomlPath = join5(projectDir, ".codex/config.toml");
1539
1349
  writer.write(
1540
1350
  configTomlPath,
1541
1351
  renderConfigToml({
@@ -1545,27 +1355,27 @@ function runCodexTransform(params) {
1545
1355
  mcp
1546
1356
  })
1547
1357
  );
1548
- const hookDir = join6(projectDir, ".codex/hooks");
1358
+ const hookDir = join5(projectDir, ".codex/hooks");
1549
1359
  const hookFiles = [];
1550
1360
  for (const hook of HOOK_NAMES) {
1551
- const src = join6(harnessRoot, "templates/hooks", `${hook}.sh`);
1552
- if (!existsSync7(src)) {
1361
+ const src = join5(harnessRoot, "templates/hooks", `${hook}.sh`);
1362
+ if (!existsSync6(src)) {
1553
1363
  continue;
1554
1364
  }
1555
- const ported = readFileSync6(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
1556
- 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`);
1557
1367
  if (!writer.write(target, ported)) continue;
1558
1368
  chmodSync(target, 493);
1559
1369
  hookFiles.push(target);
1560
1370
  }
1561
1371
  const skillFiles = [];
1562
1372
  for (const id of selectedInternalSkills) {
1563
- const src = join6(harnessRoot, "templates/skills", id, "SKILL.md");
1564
- if (!existsSync7(src)) {
1373
+ const src = join5(harnessRoot, "templates/skills", id, "SKILL.md");
1374
+ if (!existsSync6(src)) {
1565
1375
  continue;
1566
1376
  }
1567
- const target = join6(projectDir, ".agents", "skills", id, "SKILL.md");
1568
- 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;
1569
1379
  skillFiles.push(target);
1570
1380
  }
1571
1381
  return {
@@ -1577,17 +1387,17 @@ function runCodexTransform(params) {
1577
1387
  };
1578
1388
  }
1579
1389
  function readRequired(path) {
1580
- if (!existsSync7(path)) {
1390
+ if (!existsSync6(path)) {
1581
1391
  throw new Error(`Codex transform: required source missing: ${path}`);
1582
1392
  }
1583
- return readFileSync6(path, "utf8");
1393
+ return readFileSync5(path, "utf8");
1584
1394
  }
1585
1395
  function readOptionalJson(path) {
1586
- if (!existsSync7(path)) {
1396
+ if (!existsSync6(path)) {
1587
1397
  return null;
1588
1398
  }
1589
1399
  try {
1590
- return JSON.parse(readFileSync6(path, "utf8"));
1400
+ return JSON.parse(readFileSync5(path, "utf8"));
1591
1401
  } catch {
1592
1402
  return null;
1593
1403
  }
@@ -1595,8 +1405,8 @@ function readOptionalJson(path) {
1595
1405
 
1596
1406
  // src/opencode/transform.ts
1597
1407
  init_esm_shims();
1598
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
1599
- 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";
1600
1410
 
1601
1411
  // src/opencode/agents-md.ts
1602
1412
  init_esm_shims();
@@ -1605,7 +1415,7 @@ function renameSlashes2(text) {
1605
1415
  }
1606
1416
  function renderAgentsMd2(params) {
1607
1417
  const body = params.claudeMd.replace(/^#\s+.*\r?\n/, "").trim();
1608
- 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 ?? "");
1609
1419
  return renameSlashes2(replaced);
1610
1420
  }
1611
1421
 
@@ -1696,39 +1506,50 @@ function parseTemplate(template) {
1696
1506
 
1697
1507
  // src/opencode/transform.ts
1698
1508
  function runOpencodeTransform(params) {
1699
- const { harnessRoot, projectDir, selectedInternalSkills = [], baseline, refreshOnly } = params;
1509
+ const {
1510
+ harnessRoot,
1511
+ projectDir,
1512
+ selectedInternalSkills = [],
1513
+ rules = [],
1514
+ baseline,
1515
+ refreshOnly
1516
+ } = params;
1700
1517
  const writer = createOwnedWriter(projectDir, baseline, { refreshOnly: refreshOnly ?? false });
1701
- const claudeMd = readRequired2(join7(harnessRoot, "templates/CLAUDE.md"));
1702
- 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"));
1703
1520
  const opencodeTemplate = readRequired2(
1704
- join7(harnessRoot, "templates/opencode/opencode.json.template")
1521
+ join6(harnessRoot, "templates/opencode/opencode.json.template")
1705
1522
  );
1706
1523
  const projectName = basename3(projectDir);
1707
- const mcp = readOptionalJson2(join7(harnessRoot, ".mcp.json"));
1524
+ const mcp = readOptionalJson2(join6(harnessRoot, ".mcp.json"));
1708
1525
  ensureDir(projectDir);
1709
- const agentsMdPath = join7(projectDir, "AGENTS.md");
1526
+ const agentsMdPath = join6(projectDir, "AGENTS.md");
1710
1527
  const agentsMdOut = renderAgentsMd2({
1711
1528
  template: agentsTemplate,
1712
1529
  claudeMd,
1713
1530
  projectName,
1714
- projectContext: renderFillScaffold()
1531
+ projectContext: renderFillScaffold(),
1532
+ // codex 와 **같은 파일**(프로젝트 루트 `AGENTS.md`)이다. 두 transform 이 서로 다른 본문을
1533
+ // 쓰면 나중에 도는 쪽이 앞선 쪽을 덮어써, codex+opencode 조합에서 룰이 통째로 사라진다
1534
+ // (독립 검증 C-1 실측). 같은 내용을 쓰면 순서가 결과를 바꾸지 않는다.
1535
+ harnessRules: renderRulesBlock(portRules(harnessRoot, rules))
1715
1536
  });
1716
1537
  writer.write(agentsMdPath, agentsMdOut);
1717
- const opencodeJsonPath = join7(projectDir, "opencode.json");
1538
+ const opencodeJsonPath = join6(projectDir, "opencode.json");
1718
1539
  writer.write(opencodeJsonPath, renderOpencodeJson({ template: opencodeTemplate, mcp }));
1719
- const cmdDir = join7(projectDir, ".opencode/commands");
1540
+ const cmdDir = join6(projectDir, ".opencode/commands");
1720
1541
  if (!refreshOnly) ensureDir(cmdDir);
1721
1542
  const commandFiles = [];
1722
1543
  for (const id of selectedInternalSkills) {
1723
- const src = join7(harnessRoot, "templates/skills", id, "SKILL.md");
1724
- if (!existsSync8(src)) {
1544
+ const src = join6(harnessRoot, "templates/skills", id, "SKILL.md");
1545
+ if (!existsSync7(src)) {
1725
1546
  continue;
1726
1547
  }
1727
- const target = join7(cmdDir, `${id}.md`);
1728
- 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"));
1729
1550
  const wrote = writer.write(
1730
1551
  target,
1731
- renderCommandFromSkill(readFileSync7(src, "utf8"), id, { shellDependent })
1552
+ renderCommandFromSkill(readFileSync6(src, "utf8"), id, { shellDependent })
1732
1553
  );
1733
1554
  if (!wrote) continue;
1734
1555
  commandFiles.push(target);
@@ -1741,17 +1562,17 @@ function runOpencodeTransform(params) {
1741
1562
  };
1742
1563
  }
1743
1564
  function readRequired2(path) {
1744
- if (!existsSync8(path)) {
1565
+ if (!existsSync7(path)) {
1745
1566
  throw new Error(`OpenCode transform: required source missing: ${path}`);
1746
1567
  }
1747
- return readFileSync7(path, "utf8");
1568
+ return readFileSync6(path, "utf8");
1748
1569
  }
1749
1570
  function readOptionalJson2(path) {
1750
- if (!existsSync8(path)) {
1571
+ if (!existsSync7(path)) {
1751
1572
  return null;
1752
1573
  }
1753
1574
  try {
1754
- return JSON.parse(readFileSync7(path, "utf8"));
1575
+ return JSON.parse(readFileSync6(path, "utf8"));
1755
1576
  } catch {
1756
1577
  return null;
1757
1578
  }
@@ -1765,6 +1586,7 @@ function runCliTransforms(params) {
1765
1586
  projectDir,
1766
1587
  cli: cli2,
1767
1588
  selectedInternalSkills,
1589
+ rules,
1768
1590
  previousExternal,
1769
1591
  refreshOnly = false,
1770
1592
  codexTrust = false
@@ -1790,6 +1612,7 @@ function runCliTransforms(params) {
1790
1612
  harnessRoot,
1791
1613
  projectDir,
1792
1614
  selectedInternalSkills,
1615
+ rules,
1793
1616
  baseline,
1794
1617
  refreshOnly
1795
1618
  });
@@ -1804,6 +1627,7 @@ function runCliTransforms(params) {
1804
1627
  harnessRoot,
1805
1628
  projectDir,
1806
1629
  selectedInternalSkills,
1630
+ rules,
1807
1631
  baseline,
1808
1632
  refreshOnly
1809
1633
  });
@@ -1815,6 +1639,7 @@ function runCliTransforms(params) {
1815
1639
  harnessRoot,
1816
1640
  projectDir,
1817
1641
  selectedInternalSkills,
1642
+ rules,
1818
1643
  baseline,
1819
1644
  refreshOnly
1820
1645
  });
@@ -1834,8 +1659,8 @@ function runCliTransforms(params) {
1834
1659
 
1835
1660
  // src/env-files.ts
1836
1661
  init_esm_shims();
1837
- import { appendFileSync, existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
1838
- 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";
1839
1664
  var ENV_EXAMPLE_BODY = `# .env.example \u2014 csr-supabase Track
1840
1665
  # Copy to .env (gitignored) and fill in values: cp .env.example .env
1841
1666
 
@@ -1872,19 +1697,19 @@ function writeEnvExample(projectDir, tracks) {
1872
1697
  if (!tracks.some((t) => ENV_EXAMPLE_TRACKS.includes(t))) {
1873
1698
  return false;
1874
1699
  }
1875
- const path = join8(projectDir, ".env.example");
1876
- if (existsSync9(path)) {
1700
+ const path = join7(projectDir, ".env.example");
1701
+ if (existsSync8(path)) {
1877
1702
  return false;
1878
1703
  }
1879
- writeFileSync4(path, ENV_EXAMPLE_BODY);
1704
+ writeFileSync3(path, ENV_EXAMPLE_BODY);
1880
1705
  return true;
1881
1706
  }
1882
1707
  function addGitignoreEnv(projectDir) {
1883
- const path = join8(projectDir, ".gitignore");
1884
- if (!existsSync9(path)) {
1708
+ const path = join7(projectDir, ".gitignore");
1709
+ if (!existsSync8(path)) {
1885
1710
  return false;
1886
1711
  }
1887
- const content = readFileSync8(path, "utf8");
1712
+ const content = readFileSync7(path, "utf8");
1888
1713
  if (GITIGNORE_ENV_PATTERN.test(content)) {
1889
1714
  return false;
1890
1715
  }
@@ -1898,11 +1723,11 @@ function addGitignoreEnv(projectDir) {
1898
1723
  var AGENT_ARTIFACT_DIRS = [".factory/", ".goose/", ".uzys-agent-harness/"];
1899
1724
  var GITIGNORE_AGENT_ARTIFACT_HEADER = "# agent CLI / harness \uC790\uB3D9 \uC0DD\uC131\uBB3C (auto-added by agent-harness)";
1900
1725
  function addGitignoreAgentArtifacts(projectDir) {
1901
- const path = join8(projectDir, ".gitignore");
1902
- if (!existsSync9(path)) {
1726
+ const path = join7(projectDir, ".gitignore");
1727
+ if (!existsSync8(path)) {
1903
1728
  return [];
1904
1729
  }
1905
- const content = readFileSync8(path, "utf8");
1730
+ const content = readFileSync7(path, "utf8");
1906
1731
  const missing = AGENT_ARTIFACT_DIRS.filter((pattern) => {
1907
1732
  const lineRegex = new RegExp(`^${pattern.replace(/\./g, "\\.").replace(/\//g, "/")}\\s*$`, "m");
1908
1733
  return !lineRegex.test(content);
@@ -1917,43 +1742,13 @@ ${block}
1917
1742
  `);
1918
1743
  return [...missing];
1919
1744
  }
1920
- function writeMcpAllowlist(projectDir) {
1921
- const allowlistPath = join8(projectDir, ".mcp-allowlist");
1922
- if (existsSync9(allowlistPath)) {
1923
- return null;
1924
- }
1925
- const mcpPath = join8(projectDir, ".mcp.json");
1926
- if (!existsSync9(mcpPath)) {
1927
- return null;
1928
- }
1929
- let names = [];
1930
- try {
1931
- const parsed = JSON.parse(readFileSync8(mcpPath, "utf8"));
1932
- names = Object.keys(parsed.mcpServers ?? {}).sort();
1933
- } catch {
1934
- return null;
1935
- }
1936
- if (names.length === 0) {
1937
- return [];
1938
- }
1939
- const body = [
1940
- "# MCP Server Allowlist \u2014 auto-generated by agent-harness install",
1941
- "# Referenced by mcp-pre-exec.sh hook. Remove or '#' comment any server you want to block.",
1942
- "# Deleting this file disables the gate (allows all MCP calls).",
1943
- "",
1944
- ...names,
1945
- ""
1946
- ].join("\n");
1947
- writeFileSync4(allowlistPath, body);
1948
- return names;
1949
- }
1950
1745
 
1951
1746
  // src/external-installer.ts
1952
1747
  init_esm_shims();
1953
1748
  import { spawnSync } from "child_process";
1954
- import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync9 } from "fs";
1749
+ import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync8 } from "fs";
1955
1750
  import { homedir as homedir2 } from "os";
1956
- import { join as join9 } from "path";
1751
+ import { join as join8 } from "path";
1957
1752
  var DEFAULT_SPAWN_TIMEOUT_MS = 12e4;
1958
1753
  function selectExternalTargets(assets, ctx) {
1959
1754
  const conditionPassed = filterApplicableAssets(assets, ctx).filter(
@@ -2029,8 +1824,8 @@ function installOne(asset, ctx) {
2029
1824
  cwd
2030
1825
  );
2031
1826
  case "shell-script": {
2032
- const scriptPath = join9(ctx.harnessRoot, method.script);
2033
- if (!existsSync10(scriptPath)) {
1827
+ const scriptPath = join8(ctx.harnessRoot, method.script);
1828
+ if (!existsSync9(scriptPath)) {
2034
1829
  return {
2035
1830
  asset,
2036
1831
  ok: false,
@@ -2120,17 +1915,17 @@ function detectVersion(method, spawn) {
2120
1915
  if (at2 <= 0) return void 0;
2121
1916
  const plugin = method.pluginId.slice(0, at2);
2122
1917
  const marketplaceShort = method.pluginId.slice(at2 + 1);
2123
- const cacheBase = join9(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
2124
- if (!existsSync10(cacheBase)) return void 0;
2125
- 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();
2126
1921
  return versions.at(-1);
2127
1922
  }
2128
1923
  case "npm": {
2129
1924
  const npmRoot = getNpmGlobalRoot(spawn);
2130
1925
  if (!npmRoot) return void 0;
2131
- const pkgJson = join9(npmRoot, method.pkg, "package.json");
2132
- if (!existsSync10(pkgJson)) return void 0;
2133
- 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"));
2134
1929
  return parsed.version;
2135
1930
  }
2136
1931
  default:
@@ -2157,7 +1952,7 @@ function getNpmGlobalRoot(spawn) {
2157
1952
 
2158
1953
  // src/mcp-merge.ts
2159
1954
  init_esm_shims();
2160
- 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";
2161
1956
  function parseTrackMcpMap(raw) {
2162
1957
  const rows = [];
2163
1958
  for (const line of raw.split(/\r?\n/)) {
@@ -2208,15 +2003,15 @@ function mergeMcpServers(base, rows, tracks) {
2208
2003
  return out;
2209
2004
  }
2210
2005
  function composeMcpJson(opts) {
2211
- const base = JSON.parse(readFileSync10(opts.templateMcpPath, "utf8"));
2212
- const merged = opts.existingPath && existsSync11(opts.existingPath) ? mergeUserBase(base, opts.existingPath) : base;
2213
- 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") : "";
2214
2009
  const rows = parseTrackMcpMap(mapRaw);
2215
2010
  return mergeMcpServers(merged, rows, opts.tracks);
2216
2011
  }
2217
2012
  function mergeUserBase(base, existingPath) {
2218
2013
  try {
2219
- const existing = JSON.parse(readFileSync10(existingPath, "utf8"));
2014
+ const existing = JSON.parse(readFileSync9(existingPath, "utf8"));
2220
2015
  return {
2221
2016
  ...base,
2222
2017
  mcpServers: { ...base.mcpServers, ...existing.mcpServers }
@@ -2226,23 +2021,58 @@ function mergeUserBase(base, existingPath) {
2226
2021
  }
2227
2022
  }
2228
2023
  function writeMcpJson(path, mcp) {
2229
- writeFileSync5(path, `${JSON.stringify(mcp, null, 2)}
2024
+ writeFileSync4(path, `${JSON.stringify(mcp, null, 2)}
2230
2025
  `);
2231
2026
  }
2232
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
+
2233
2063
  // src/update-mode.ts
2234
2064
  init_esm_shims();
2235
2065
  import {
2236
- copyFileSync as copyFileSync2,
2066
+ copyFileSync,
2237
2067
  existsSync as existsSync12,
2238
2068
  lstatSync,
2239
- mkdirSync as mkdirSync5,
2240
- readdirSync as readdirSync3,
2069
+ mkdirSync as mkdirSync3,
2070
+ readdirSync as readdirSync2,
2241
2071
  readFileSync as readFileSync11,
2242
2072
  unlinkSync,
2243
- writeFileSync as writeFileSync6
2073
+ writeFileSync as writeFileSync5
2244
2074
  } from "fs";
2245
- 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";
2246
2076
  var LEGACY_ANCHOR_FILE = ".claude/CLAUDE.md";
2247
2077
  function buildUpdateSpec(projectDir, tracks) {
2248
2078
  return {
@@ -2269,7 +2099,8 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot) {
2269
2099
  externalBackedUp: [],
2270
2100
  installedNew: [],
2271
2101
  restored: [],
2272
- needsReinstall: []
2102
+ needsReinstall: [],
2103
+ mcpAllowlistRetired: null
2273
2104
  };
2274
2105
  const fresh = installNewAssets(projectDir, templatesDir);
2275
2106
  report.installedNew = fresh.installed;
@@ -2296,11 +2127,15 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot) {
2296
2127
  report.skillsBackedUp = skillSync.backedUp;
2297
2128
  report.skillsSkippedLinks = skillSync.skippedLinks;
2298
2129
  refreshSkillBaseline(projectDir);
2299
- 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
+ }
2300
2134
  const settingsPath = join10(claudeDir, "settings.json");
2301
2135
  if (existsSync12(settingsPath)) {
2302
2136
  report.staleHookRefs = cleanStaleHookRefs(settingsPath, claudeDir);
2303
2137
  }
2138
+ report.mcpAllowlistRetired = retireMcpAllowlist(projectDir);
2304
2139
  const external = refreshExternalCli(projectDir, harnessRoot);
2305
2140
  report.externalUpdated = external.externalUpdated;
2306
2141
  report.externalBackedUp = external.externalBackedUp;
@@ -2310,10 +2145,13 @@ function installNewAssets(projectDir, templatesDir) {
2310
2145
  const installed = [];
2311
2146
  const restored = [];
2312
2147
  const needsReinstall = [];
2313
- 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 ?? []);
2314
2151
  const priorBaseline = policyBaseline(projectDir);
2315
2152
  for (const entry of trackOnlyFileAssets(installedTracks(projectDir))) {
2316
2153
  if (entry.target === HARNESS_ANCHOR_FILE) continue;
2154
+ if (isBaselineExcluded(entry.target, baselineExcluded)) continue;
2317
2155
  const target = join10(projectDir, entry.target);
2318
2156
  if (existsSync12(target)) continue;
2319
2157
  const source = join10(templatesDir, entry.source);
@@ -2325,8 +2163,8 @@ function installNewAssets(projectDir, templatesDir) {
2325
2163
  continue;
2326
2164
  }
2327
2165
  }
2328
- mkdirSync5(dirname5(target), { recursive: true });
2329
- copyFileSync2(source, target);
2166
+ mkdirSync3(dirname3(target), { recursive: true });
2167
+ copyFileSync(source, target);
2330
2168
  const recordedAs = entry.target.startsWith(".claude/") ? entry.target.slice(8) : entry.target;
2331
2169
  if (priorBaseline.has(recordedAs)) restored.push(entry.target);
2332
2170
  else installed.push(entry.target);
@@ -2349,7 +2187,7 @@ function syncHarnessAnchor(projectDir, templatesDir, report) {
2349
2187
  if (!existsSync12(templateMd)) return;
2350
2188
  const anchor = join10(projectDir, HARNESS_ANCHOR_FILE);
2351
2189
  const existed = existsSync12(anchor);
2352
- copyFileSync2(templateMd, anchor);
2190
+ copyFileSync(templateMd, anchor);
2353
2191
  if (existed) {
2354
2192
  report.claudeMdUpdated = true;
2355
2193
  return;
@@ -2366,7 +2204,7 @@ function upsertRootImport(projectDir) {
2366
2204
  tracks: installedTracks(projectDir)
2367
2205
  });
2368
2206
  if (next === existing) return false;
2369
- writeFileSync6(target, next);
2207
+ writeFileSync5(target, next);
2370
2208
  return true;
2371
2209
  }
2372
2210
  function installedTracks(projectDir) {
@@ -2393,11 +2231,14 @@ function recordAnchorBaseline(projectDir, anchor) {
2393
2231
  }
2394
2232
  function refreshExternalCli(projectDir, harnessRoot) {
2395
2233
  const log = readInstallLog(projectDir);
2234
+ const baselineExcluded = new Set(log?.spec.baselineExclude ?? []);
2396
2235
  const result = runCliTransforms({
2397
2236
  harnessRoot,
2398
2237
  projectDir,
2399
2238
  cli: ALL_CLI_TARGETS,
2400
2239
  selectedInternalSkills: INTERNAL_BUNDLED_SKILL_IDS,
2240
+ // 스킬은 **전부** 넘긴다 — 안 깔린 스킬은 대상 파일이 없어 refreshOnly 가 건너뛴다.
2241
+ rules: ALL_RULES.filter((r) => !isBaselineExcluded(`.claude/rules/${r}.md`, baselineExcluded)),
2401
2242
  previousExternal: log?.externalFiles ?? [],
2402
2243
  refreshOnly: true
2403
2244
  });
@@ -2423,7 +2264,7 @@ function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2423
2264
  if (!existsSync12(target) || !existsSync12(source)) return { updated: 0, backedUp: [] };
2424
2265
  let updated = 0;
2425
2266
  const backedUp = [];
2426
- for (const file of readdirSync3(target)) {
2267
+ for (const file of readdirSync2(target)) {
2427
2268
  if (!file.endsWith(ext)) continue;
2428
2269
  const targetFile = join10(target, file);
2429
2270
  const sourceFile = join10(source, file);
@@ -2435,7 +2276,7 @@ function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2435
2276
  backupFile(targetFile, now);
2436
2277
  backedUp.push(`${ctx.prefix}/${file}`);
2437
2278
  }
2438
- writeFileSync6(targetFile, next);
2279
+ writeFileSync5(targetFile, next);
2439
2280
  updated++;
2440
2281
  }
2441
2282
  return { updated, backedUp };
@@ -2446,7 +2287,7 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
2446
2287
  let updated = 0;
2447
2288
  const backedUp = [];
2448
2289
  const skippedLinks = [];
2449
- for (const skill of readdirSync3(sourceDir, { withFileTypes: true })) {
2290
+ for (const skill of readdirSync2(sourceDir, { withFileTypes: true })) {
2450
2291
  if (!skill.isDirectory()) continue;
2451
2292
  const targetSkill = join10(targetDir, skill.name);
2452
2293
  if (!existsSync12(targetSkill)) continue;
@@ -2458,8 +2299,8 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
2458
2299
  const targetFile = join10(targetSkill, rel);
2459
2300
  const next = readFileSync11(join10(sourceDir, skill.name, rel), "utf8");
2460
2301
  if (!existsSync12(targetFile)) {
2461
- mkdirSync5(dirname5(targetFile), { recursive: true });
2462
- writeFileSync6(targetFile, next);
2302
+ mkdirSync3(dirname3(targetFile), { recursive: true });
2303
+ writeFileSync5(targetFile, next);
2463
2304
  updated++;
2464
2305
  continue;
2465
2306
  }
@@ -2470,7 +2311,7 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
2470
2311
  backupFile(targetFile, now);
2471
2312
  backedUp.push(`${skill.name}/${rel}`);
2472
2313
  }
2473
- writeFileSync6(targetFile, next);
2314
+ writeFileSync5(targetFile, next);
2474
2315
  updated++;
2475
2316
  }
2476
2317
  }
@@ -2511,7 +2352,7 @@ function refreshSkillBaseline(projectDir) {
2511
2352
  function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2512
2353
  if (!existsSync12(target) || !existsSync12(source)) return [];
2513
2354
  const removed = [];
2514
- for (const file of readdirSync3(target)) {
2355
+ for (const file of readdirSync2(target)) {
2515
2356
  if (!file.endsWith(ext)) continue;
2516
2357
  const sourceFile = join10(source, file);
2517
2358
  if (!existsSync12(sourceFile)) {
@@ -2528,6 +2369,17 @@ function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()
2528
2369
  }
2529
2370
  return removed;
2530
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
+ }
2531
2383
  function cleanStaleHookRefs(settingsPath, claudeDir) {
2532
2384
  let settings;
2533
2385
  try {
@@ -2550,7 +2402,7 @@ function cleanStaleHookRefs(settingsPath, claudeDir) {
2550
2402
  }
2551
2403
  if (removed.length > 0) {
2552
2404
  const next = { ...settings, hooks: cleanedHooks };
2553
- writeFileSync6(settingsPath, `${JSON.stringify(next, null, 2)}
2405
+ writeFileSync5(settingsPath, `${JSON.stringify(next, null, 2)}
2554
2406
  `);
2555
2407
  }
2556
2408
  return removed;
@@ -2590,17 +2442,27 @@ function runInstall(ctx) {
2590
2442
  throw new Error(`Templates dir not found: ${templatesDir}`);
2591
2443
  }
2592
2444
  const claudeDir = join11(projectDir, ".claude");
2593
- if (mode === "update" && !existsSync13(claudeDir)) {
2594
- throw new Error(`Update mode requires existing .claude/ at ${claudeDir}`);
2595
- }
2596
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
+ }
2597
2452
  const backupPath = resolveBackupPath(ctx, mode, claudeDir);
2598
2453
  if (mode === "update") {
2599
2454
  return runUpdateInstall(ctx, templatesDir, backupPath);
2600
2455
  }
2601
2456
  const manifestSpec = buildManifestSpec(spec);
2602
2457
  const policyBase = new Map((previousLog?.policyFiles ?? []).map((f) => [f.path, f.sha256]));
2603
- 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
+ );
2604
2466
  const mcpResult = composeAndWriteMcp(harnessRoot, projectDir, spec);
2605
2467
  const ciScaffold = isAssetSelected("ci-scaffold", {
2606
2468
  tracks: spec.tracks,
@@ -2618,6 +2480,11 @@ function runInstall(ctx) {
2618
2480
  projectDir,
2619
2481
  cli: spec.cli,
2620
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
+ ),
2621
2488
  previousExternal: previousLog?.externalFiles ?? [],
2622
2489
  codexTrust: (spec.scope ?? "project") === "global" && spec.options.withCodexTrust
2623
2490
  });
@@ -2637,7 +2504,12 @@ function runInstall(ctx) {
2637
2504
  rootClaudeMd: base.rootClaudeMd,
2638
2505
  // 외부 CLI 백업도 같은 줄에 노출한다 — 백업이 화면에 안 보이면 사용자는 자기 편집분이
2639
2506
  // 어디 갔는지 알 수 없고, 그러면 백업은 있어도 없는 것과 같다 (ADR-046/047 과 같은 이유).
2640
- 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)
2641
2513
  };
2642
2514
  ctx.onProgress?.({ type: "baseline-complete", baseline });
2643
2515
  const external = runExternalPhase(ctx);
@@ -2653,6 +2525,21 @@ function runInstall(ctx) {
2653
2525
  );
2654
2526
  return { ...baseline, external, staleHookRefs };
2655
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
+ }
2656
2543
  function healStaleHookRefs(spec, projectDir) {
2657
2544
  if (!spec.cli.includes("claude")) return [];
2658
2545
  const settingsPath = join11(projectDir, ".claude/settings.json");
@@ -2670,6 +2557,11 @@ function runUpdateInstall(ctx, templatesDir, backupPath) {
2670
2557
  filesCopied: 0,
2671
2558
  dirsCopied: 0,
2672
2559
  skipped: 0,
2560
+ baselineExcluded: [],
2561
+ baselineExcludedOnDisk: [],
2562
+ // update 는 밀려난 자산을 다루지 않는다 — 그 판정은 사용자가 옵션을 고르는 설치 경로의
2563
+ // 것이고, update 는 무엇을 골랐는지 다시 묻지 않는다.
2564
+ superseded: { removed: [], kept: [] },
2673
2565
  backup: backupPath,
2674
2566
  installedTracks: [...ctx.spec.tracks].sort(),
2675
2567
  mcpServers: [],
@@ -2683,7 +2575,6 @@ function runUpdateInstall(ctx, templatesDir, backupPath) {
2683
2575
  envFiles: {
2684
2576
  envExampleCreated: false,
2685
2577
  gitignoreEnvAdded: false,
2686
- mcpAllowlist: null,
2687
2578
  gitignoreNpxSkillsAdded: []
2688
2579
  },
2689
2580
  rootClaudeMd: null
@@ -2720,9 +2611,30 @@ function emptyClaudeBaseline() {
2720
2611
  categories: { rules: [], agents: [], hooks: [], commands: 0, skills: [] },
2721
2612
  rootClaudeMd: null,
2722
2613
  rootClaudeMdLog: null,
2723
- backups: []
2614
+ backups: [],
2615
+ excluded: [],
2616
+ excludedOnDisk: []
2724
2617
  };
2725
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
+ }
2726
2638
  function backupEditedPolicyFile(entryTarget, target, source, baseline) {
2727
2639
  const prefix = ".claude/";
2728
2640
  if (!entryTarget.startsWith(prefix)) return null;
@@ -2737,7 +2649,7 @@ function backupEditedPolicyFile(entryTarget, target, source, baseline) {
2737
2649
  if (recorded !== void 0 && recorded === hashContent(current)) return null;
2738
2650
  return backupFile(target);
2739
2651
  }
2740
- function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase) {
2652
+ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase, baselineExcluded) {
2741
2653
  ensureProjectSkeleton(projectDir);
2742
2654
  const result = emptyClaudeBaseline();
2743
2655
  const manifest = buildManifest(manifestSpec);
@@ -2745,8 +2657,13 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBas
2745
2657
  if (!entry.applies(manifestSpec)) {
2746
2658
  continue;
2747
2659
  }
2748
- const source = join11(templatesDir, entry.source);
2749
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);
2750
2667
  if (!existsSync13(source)) {
2751
2668
  result.skipped += 1;
2752
2669
  continue;
@@ -2790,7 +2707,6 @@ function writeEnvironmentFiles(projectDir, tracks) {
2790
2707
  return {
2791
2708
  envExampleCreated: writeEnvExample(projectDir, tracks),
2792
2709
  gitignoreEnvAdded: addGitignoreEnv(projectDir),
2793
- mcpAllowlist: writeMcpAllowlist(projectDir),
2794
2710
  // v0.8.0 — `.factory/`, `.goose/` ignore (npx skills universal install 사용자 #3).
2795
2711
  // 2026-08-02 — `.uzys-agent-harness/` 합류 (설치 로그 + 훅 차단 로그).
2796
2712
  gitignoreNpxSkillsAdded: addGitignoreAgentArtifacts(projectDir)
@@ -2882,13 +2798,6 @@ function collectRootFiles(envFiles, ciScaffold, mcpCreated) {
2882
2798
  if (envFiles.envExampleCreated) {
2883
2799
  files.push({ path: ".env.example", change: "created", notes: ["Supabase \uD1A0\uD070 \uAC00\uC774\uB4DC"] });
2884
2800
  }
2885
- if (envFiles.mcpAllowlist && envFiles.mcpAllowlist.length > 0) {
2886
- files.push({
2887
- path: ".mcp-allowlist",
2888
- change: "created",
2889
- notes: [`MCP allowlist (${envFiles.mcpAllowlist.length} server)`]
2890
- });
2891
- }
2892
2801
  const gitignoreAdded = [
2893
2802
  ...envFiles.gitignoreEnvAdded ? [".env"] : [],
2894
2803
  ...envFiles.gitignoreNpxSkillsAdded
@@ -2925,9 +2834,9 @@ function accumulateCategory(cats, entry) {
2925
2834
  }
2926
2835
  function writeInstalledTracks(projectDir, tracks) {
2927
2836
  const path = join11(projectDir, ".claude/.installed-tracks");
2928
- mkdirSync6(dirname6(path), { recursive: true });
2837
+ mkdirSync4(dirname4(path), { recursive: true });
2929
2838
  const sorted = [...new Set(tracks)].sort().join("\n");
2930
- writeFileSync7(path, `${sorted}
2839
+ writeFileSync6(path, `${sorted}
2931
2840
  `);
2932
2841
  }
2933
2842
  function writeRootClaudeMd(projectDir, tracks) {
@@ -2935,7 +2844,7 @@ function writeRootClaudeMd(projectDir, tracks) {
2935
2844
  const existing = existsSync13(target) ? readFileSync12(target, "utf-8") : null;
2936
2845
  const content = upsertHarnessImport(existing, { projectName: basename5(projectDir), tracks });
2937
2846
  if (content !== existing) {
2938
- writeFileSync7(target, content);
2847
+ writeFileSync6(target, content);
2939
2848
  }
2940
2849
  return { created: existing === null };
2941
2850
  }
@@ -2948,7 +2857,7 @@ function chmodHooksSync(hookDir) {
2948
2857
  }
2949
2858
  }
2950
2859
  function listHookFiles(hookDir) {
2951
- 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));
2952
2861
  }
2953
2862
 
2954
2863
  // src/commands/install-render.ts
@@ -3301,6 +3210,15 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
3301
3210
  )
3302
3211
  );
3303
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
+ }
3304
3222
  if (baseline.updateMode.policyBackedUp.length > 0) {
3305
3223
  log(
3306
3224
  assetRow(
@@ -3375,12 +3293,12 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
3375
3293
  phase1Row(
3376
3294
  "hooks",
3377
3295
  cats.hooks.length,
3378
- "session-start \xB7 protect-files \xB7 mcp-pre-exec (security) \xB7 task-brief-nudge",
3296
+ "session-start \xB7 protect-files \xB7 task-brief-nudge",
3379
3297
  cats.hooks
3380
3298
  );
3381
3299
  }
3382
3300
  if (cats.commands > 0) {
3383
- phase1Row("commands", cats.commands, "/ecc:* (ECC plugin OFF fallback)");
3301
+ phase1Row("commands", cats.commands, "slash commands");
3384
3302
  }
3385
3303
  if (cats.skills.length > 0) {
3386
3304
  phase1Row(
@@ -3394,6 +3312,38 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
3394
3312
  log(assetRow("success", "rules + hooks + commands + agents", `${baseline.filesCopied} files`));
3395
3313
  log(assetRow("success", "skeleton", `${baseline.dirsCopied} dirs`));
3396
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
+ }
3397
3347
  const TEMPLATES_COL = 28;
3398
3348
  if (baseline.rootClaudeMd) {
3399
3349
  const n = baseline.rootClaudeMd.tracks.length;
@@ -3429,16 +3379,6 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
3429
3379
  }
3430
3380
  const mcpList = baseline.mcpServers.join(", ") || "(none)";
3431
3381
  log(assetRow("success", ".mcp.json", mcpList, TEMPLATES_COL));
3432
- if (baseline.envFiles.mcpAllowlist) {
3433
- log(
3434
- assetRow(
3435
- "success",
3436
- ".mcp-allowlist",
3437
- `${baseline.envFiles.mcpAllowlist.length} servers (D35 opt-in gate)`,
3438
- TEMPLATES_COL
3439
- )
3440
- );
3441
- }
3442
3382
  if (!withEcc && baseline.categories) {
3443
3383
  log("");
3444
3384
  log(
@@ -3550,15 +3490,26 @@ function installAction(options, deps = {}) {
3550
3490
  }
3551
3491
  const forceInclude = normalizeRepeatable(options.with);
3552
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));
3553
3497
  const validIds = new Set(EXTERNAL_ASSETS.map((a) => a.id));
3554
- for (const id of [...forceInclude, ...forceExclude]) {
3555
- if (!validIds.has(id)) {
3556
- err(
3557
- c.yellow(
3558
- `[WARN] Unknown asset id '${id}' (--with/--without). Skipping. Use one of: ${[...validIds].sort().join(", ")}`
3559
- )
3560
- );
3561
- }
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
+ );
3562
3513
  }
3563
3514
  const filteredInclude = forceInclude.filter((id) => validIds.has(id));
3564
3515
  const filteredExclude = forceExclude.filter((id) => validIds.has(id));
@@ -3566,6 +3517,7 @@ function installAction(options, deps = {}) {
3566
3517
  const spec = {
3567
3518
  tracks: options.track ?? [],
3568
3519
  ...userOverride ? { userOverride } : {},
3520
+ ...baselineExclude.length > 0 ? { baselineExclude } : {},
3569
3521
  // v26.81.0 (ADR-022, BREAKING) — 자산 1:1 boolean 13종 삭제. 자산 선택은 위
3570
3522
  // userOverride(--with <id>)로 일원화. 잔존 = 설치 동작 옵션만.
3571
3523
  options: {
@@ -3763,7 +3715,7 @@ function registerListCommand(cli2) {
3763
3715
  // src/commands/uninstall.ts
3764
3716
  init_esm_shims();
3765
3717
  import { spawnSync as spawnSync2 } from "child_process";
3766
- 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";
3767
3719
  import { join as join13, resolve as resolve4 } from "path";
3768
3720
 
3769
3721
  // src/uninstall-interactive.ts
@@ -5165,7 +5117,7 @@ function stripRootImport(projectDir) {
5165
5117
  const stripped = current === null ? null : stripHarnessImport(current);
5166
5118
  if (stripped === null) return false;
5167
5119
  try {
5168
- writeFileSync8(join13(projectDir, "CLAUDE.md"), stripped, "utf8");
5120
+ writeFileSync7(join13(projectDir, "CLAUDE.md"), stripped, "utf8");
5169
5121
  return true;
5170
5122
  } catch {
5171
5123
  return false;
@@ -5202,7 +5154,7 @@ function defaultSpawn2(cmd, args) {
5202
5154
  }
5203
5155
  function defaultRm(path) {
5204
5156
  if (existsSync15(path)) {
5205
- rmSync2(path, { recursive: true, force: true });
5157
+ rmSync(path, { recursive: true, force: true });
5206
5158
  }
5207
5159
  }
5208
5160
  function registerUninstallCommand(cli2) {
@@ -5430,6 +5382,18 @@ var CLI_BASE_LABELS = {
5430
5382
  antigravity: "Antigravity (Google)"
5431
5383
  };
5432
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
+ },
5433
5397
  { label: "Dev Core (Frontend \xB7 Backend \xB7 Data)", cats: ["frontend", "backend", "data"] },
5434
5398
  { label: "Dev Tools (Security \xB7 Quality \xB7 Understanding)", cats: ["dev-tools", "understanding"] },
5435
5399
  { label: "Business (PM \xB7 Executive \xB7 Documents)", cats: ["business"] },
@@ -5450,10 +5414,25 @@ function expandDevMethodBundle(ids) {
5450
5414
  const rest = ids.filter((v2) => v2 !== DEV_METHOD_BUNDLE_VALUE);
5451
5415
  return ids.includes(DEV_METHOD_BUNDLE_VALUE) ? [...rest, ...bundleMemberValues()] : rest;
5452
5416
  }
5453
- 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 = []) {
5454
5424
  const installedMark = (value) => installedSet.has(value) ? " \u25CF installed" : "";
5455
5425
  const groups = {};
5456
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
+ }
5457
5436
  for (const cat of cats) {
5458
5437
  const items = [];
5459
5438
  for (const o of VISIBLE_OPTION_DEFS.filter((d2) => d2.category === cat)) {
@@ -5588,8 +5567,26 @@ Proceed?`,
5588
5567
  });
5589
5568
  return q(result) ? null : result;
5590
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
+ },
5591
5587
  selectInstallTargets: async (initialChecked, step, recap) => {
5592
5588
  const pages = INSTALL_TARGET_PAGES;
5589
+ const baselineTargets = listBaselineTargets({ tracks: recap?.tracks ?? [] });
5593
5590
  const displayInitial = collapseDevMethodBundle(initialChecked);
5594
5591
  const initialSet = new Set(displayInitial);
5595
5592
  const collected = new Set(displayInitial);
@@ -5602,10 +5599,25 @@ Proceed?`,
5602
5599
  let aborted = false;
5603
5600
  try {
5604
5601
  let pageIdx = 0;
5602
+ let dir = 1;
5605
5603
  while (pageIdx < pages.length) {
5606
5604
  const page = pages[pageIdx];
5607
5605
  if (!page) break;
5608
- 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
+ }
5609
5621
  const selectedNow = flatItems.filter((it3) => collected.has(it3.value)).map((it3) => it3.value);
5610
5622
  const pageDefault = flatItems.filter((it3) => initialSet.has(it3.value)).length;
5611
5623
  const totalSelected = collected.size;
@@ -5631,11 +5643,13 @@ Proceed?`,
5631
5643
  aborted = true;
5632
5644
  break;
5633
5645
  }
5646
+ dir = -1;
5634
5647
  pageIdx--;
5635
5648
  continue;
5636
5649
  }
5637
5650
  for (const it3 of flatItems) collected.delete(it3.value);
5638
5651
  for (const v2 of result) collected.add(v2);
5652
+ dir = 1;
5639
5653
  pageIdx++;
5640
5654
  }
5641
5655
  if (!aborted) {
@@ -5659,14 +5673,21 @@ Proceed?`,
5659
5673
  function splitInstallTargets(targets) {
5660
5674
  const optionKeys = [];
5661
5675
  const assetIds = [];
5676
+ const baselineIds = [];
5662
5677
  for (const t of targets) {
5663
5678
  if (t.startsWith("option:")) {
5664
5679
  optionKeys.push(t.slice("option:".length));
5665
5680
  } else if (t.startsWith("asset:")) {
5666
5681
  assetIds.push(t.slice("asset:".length));
5682
+ } else if (t.startsWith(BASELINE_PREFIX)) {
5683
+ baselineIds.push(t);
5667
5684
  }
5668
5685
  }
5669
- 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);
5670
5691
  }
5671
5692
  function toOptionFlags(keys) {
5672
5693
  const picked = new Set(keys);
@@ -5686,7 +5707,9 @@ function installedTargetState(projectDir) {
5686
5707
  function initialTargetSelection(tracks, installedProjectAssetIds) {
5687
5708
  const ids = new Set(recommendedExternalAssets(tracks));
5688
5709
  for (const id of installedProjectAssetIds) ids.add(id);
5689
- 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];
5690
5713
  }
5691
5714
  async function runInteractive(projectDir, deps = {}) {
5692
5715
  const prompts = deps.prompts ?? defaultPrompts;
@@ -5788,16 +5811,21 @@ ${formatSummary(spec)}`
5788
5811
  } else {
5789
5812
  const finalTracks = tracks;
5790
5813
  const finalCli = cli2;
5791
- const { optionKeys, assetIds } = splitInstallTargets(targetSelections ?? []);
5814
+ const { optionKeys, assetIds, baselineIds } = splitInstallTargets(targetSelections ?? []);
5792
5815
  const options = toOptionFlags(optionKeys);
5793
5816
  const userOverride = targetSelections === null ? void 0 : computeUserOverride(finalTracks, assetIds);
5817
+ const baselineExclude = targetSelections === null ? [] : baselineExcludeFrom(listBaselineTargets({ tracks: finalTracks }), baselineIds);
5794
5818
  const scopeLabel = scope === "global" ? "Global (writes to ~/.claude/, ~/.codex/, npm -g)" : "Project (current directory only)";
5795
5819
  const summary = `${formatSummary({
5796
5820
  tracks: finalTracks,
5797
5821
  options,
5798
5822
  cli: finalCli,
5799
5823
  projectDir,
5800
- ...userOverride ? { userOverride } : {}
5824
+ ...userOverride ? { userOverride } : {},
5825
+ // 해제 목록을 안 넘기면 이 화면이 **제외 유무와 문자열이 완전히 같아진다** — 같은 화면이
5826
+ // 외부 자산 제거는 `-User removed:` 로 이미 보고하므로, 없음은 "아무것도 안 빠졌다"로
5827
+ // 읽힌다. 상주 비용을 줄이려고 20개를 푼 사용자가 그대로인 숫자를 보게 된다.
5828
+ ...baselineExclude.length > 0 ? { baselineExclude } : {}
5801
5829
  })}
5802
5830
  SCOPE ${scopeLabel}`;
5803
5831
  const confirmed = await prompts.confirmInstall(
@@ -5812,22 +5840,59 @@ ${summary}`
5812
5840
  prompts.outro("Cancelled by user.");
5813
5841
  return { ok: false, reason: "cancelled" };
5814
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
+ }
5815
5870
  prompts.outro(stepLabel(WIZARD.INSTALL, "Installing..."));
5816
5871
  return {
5817
5872
  ok: true,
5818
5873
  mode,
5819
- spec: {
5820
- tracks: finalTracks,
5821
- options,
5822
- cli: finalCli,
5823
- projectDir,
5824
- scope,
5825
- ...userOverride ? { userOverride } : {}
5826
- }
5874
+ spec: { ...spec, ...cleanSuperseded ? { cleanSuperseded } : {} }
5827
5875
  };
5828
5876
  }
5829
5877
  }
5830
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
+ }
5831
5896
  function tracksEqual(a, b2) {
5832
5897
  if (a.length !== b2.length) return false;
5833
5898
  const sortedA = [...a].sort();
@@ -5843,6 +5908,7 @@ function computeUserOverride(tracks, assetIds) {
5843
5908
  return { forceInclude, forceExclude };
5844
5909
  }
5845
5910
  function formatSummary(spec) {
5911
+ const baselineExcluded = new Set(spec.baselineExclude ?? []);
5846
5912
  const opts = Object.keys(spec.options).filter((k2) => spec.options[k2]).map((k2) => k2.replace(/^with/, "").toLowerCase());
5847
5913
  const optsLabel = opts.length > 0 ? opts.join(", ") : "(none added)";
5848
5914
  const lines = [
@@ -5864,7 +5930,11 @@ function formatSummary(spec) {
5864
5930
  lines.push(` \xB7 ${cat}: ${ids.join(", ")}`);
5865
5931
  }
5866
5932
  const cost = formatResidentCostLine(
5867
- 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
+ ),
5868
5938
  summarizeContextCost(finalAssets).unmeasuredCount
5869
5939
  );
5870
5940
  if (cost) lines.push(` \xB7 ${cost}`);
@@ -5877,6 +5947,11 @@ function formatSummary(spec) {
5877
5947
  lines.push(` -User removed: ${spec.userOverride.forceExclude.join(", ")}`);
5878
5948
  }
5879
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
+ }
5880
5955
  return lines.join("\n");
5881
5956
  }
5882
5957