@uzysjung/agent-harness 26.131.1 → 26.133.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -712,7 +712,7 @@ var cac = (name = "") => new CAC(name);
712
712
  // package.json
713
713
  var package_default = {
714
714
  name: "@uzysjung/agent-harness",
715
- version: "26.131.1",
715
+ version: "26.133.0",
716
716
  description: "Curate vetted AI-coding skills & plugins by your tech stack \u2014 install only what you need, across Claude Code, Codex, OpenCode & Antigravity",
717
717
  type: "module",
718
718
  publishConfig: {
@@ -906,18 +906,18 @@ function padDisplay(s, width) {
906
906
  init_esm_shims();
907
907
  import {
908
908
  chmodSync as chmodSync2,
909
- existsSync as existsSync12,
910
- mkdirSync as mkdirSync5,
909
+ existsSync as existsSync13,
910
+ mkdirSync as mkdirSync6,
911
911
  readdirSync as readdirSync4,
912
- readFileSync as readFileSync11,
913
- writeFileSync as writeFileSync9
912
+ readFileSync as readFileSync12,
913
+ writeFileSync as writeFileSync7
914
914
  } from "fs";
915
- import { basename as basename4, dirname as dirname5, join as join11, resolve } from "path";
915
+ import { basename as basename4, dirname as dirname6, join as join11, resolve } from "path";
916
916
 
917
917
  // src/antigravity/transform.ts
918
918
  init_esm_shims();
919
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
920
- import { basename, join as join2 } from "path";
919
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
920
+ import { basename, join as join3 } from "path";
921
921
 
922
922
  // src/codex/agents-md.ts
923
923
  init_esm_shims();
@@ -960,6 +960,11 @@ function portBody(body) {
960
960
  return renameSlashes(body).replace(/CLAUDE_PROJECT_DIR/g, "CODEX_PROJECT_DIR").trimEnd();
961
961
  }
962
962
 
963
+ // src/owned-write.ts
964
+ init_esm_shims();
965
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
966
+ import { dirname as dirname3, relative, sep } from "path";
967
+
963
968
  // src/fs-ops.ts
964
969
  init_esm_shims();
965
970
  import {
@@ -1050,6 +1055,218 @@ function ensureProjectSkeleton(projectDir) {
1050
1055
  }
1051
1056
  }
1052
1057
 
1058
+ // src/install-log.ts
1059
+ init_esm_shims();
1060
+ import { createHash } from "crypto";
1061
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
1062
+ import { dirname as dirname2, join as join2 } from "path";
1063
+ var INSTALL_LOG_FILENAME = ".harness-install.json";
1064
+ var INSTALL_LOG_VERSION = 1;
1065
+ var POLICY_DIRS = [
1066
+ { dir: "rules", ext: ".md" },
1067
+ { dir: "agents", ext: ".md" },
1068
+ { dir: "commands/uzys", ext: ".md" },
1069
+ { dir: "hooks", ext: ".sh" }
1070
+ ];
1071
+ function buildAssetEntries(report, scope) {
1072
+ if (!report) return [];
1073
+ return report.attempted.filter((r) => r.ok).map((r) => assetToLogEntry(r.asset, scope, r.version));
1074
+ }
1075
+ function assetToLogEntry(asset, scope, version) {
1076
+ const detail = methodDetail(asset.method);
1077
+ const entry = {
1078
+ id: asset.id,
1079
+ category: asset.category,
1080
+ method: asset.method.kind,
1081
+ scope,
1082
+ detail
1083
+ };
1084
+ if (version) entry.version = version;
1085
+ return entry;
1086
+ }
1087
+ function methodDetail(method) {
1088
+ switch (method.kind) {
1089
+ case "plugin":
1090
+ return { marketplace: method.marketplace, pluginId: method.pluginId };
1091
+ case "skill":
1092
+ return { source: method.source, ...method.skill ? { skill: method.skill } : {} };
1093
+ case "npm":
1094
+ return { pkg: method.pkg };
1095
+ case "npx-run":
1096
+ return { cmd: method.cmd, args: (method.args ?? []).join(" ") };
1097
+ case "shell-script":
1098
+ return { script: method.script, args: method.args.join(" ") };
1099
+ case "internal":
1100
+ return { key: method.key };
1101
+ }
1102
+ }
1103
+ function buildInstallLog(spec, external, scope, rootClaudeMd, previous, claudeDirMovedAside = false, rootFiles = []) {
1104
+ const templates = {
1105
+ claudeDir: ".claude/",
1106
+ ...spec.cli.includes("codex") ? { codexDir: ".codex/" } : {},
1107
+ ...spec.cli.includes("opencode") ? { opencodeDir: ".opencode/" } : {},
1108
+ ...rootClaudeMd ? { rootClaudeMd } : {}
1109
+ };
1110
+ const log = {
1111
+ schemaVersion: INSTALL_LOG_VERSION,
1112
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1113
+ scope,
1114
+ spec: {
1115
+ tracks: spec.tracks,
1116
+ cli: spec.cli
1117
+ },
1118
+ // 이번 설치가 만든 항목이 이기고, 이번에 안 만든 항목은 이전 값을 그대로 둔다.
1119
+ // (예: claude 로 깔고 나중에 codex 만 추가 설치해도 root CLAUDE.md 기록이 살아남는다)
1120
+ templates: { ...previous?.templates, ...templates },
1121
+ assets: mergeAssets(
1122
+ claudeDirMovedAside ? previous?.assets?.filter(survivesClaudeDirRename) : previous?.assets,
1123
+ buildAssetEntries(external, scope)
1124
+ )
1125
+ };
1126
+ const mergedRootFiles = mergeRootFiles(previous?.rootFiles, rootFiles);
1127
+ if (mergedRootFiles.length > 0) log.rootFiles = mergedRootFiles;
1128
+ return log;
1129
+ }
1130
+ function mergeRootFiles(previous, current) {
1131
+ const byPath = /* @__PURE__ */ new Map();
1132
+ for (const file of [...previous ?? [], ...current]) {
1133
+ const prior = byPath.get(file.path);
1134
+ byPath.set(
1135
+ file.path,
1136
+ prior ? {
1137
+ path: file.path,
1138
+ change: prior.change === "created" ? "created" : file.change,
1139
+ notes: [.../* @__PURE__ */ new Set([...prior.notes, ...file.notes])]
1140
+ } : file
1141
+ );
1142
+ }
1143
+ return [...byPath.values()];
1144
+ }
1145
+ function survivesClaudeDirRename(asset) {
1146
+ if (asset.scope === "global") return true;
1147
+ switch (asset.method) {
1148
+ case "plugin":
1149
+ return true;
1150
+ // `~/.claude/plugins/cache` — 프로젝트 밖
1151
+ case "npm":
1152
+ return true;
1153
+ // `node_modules/`
1154
+ case "skill":
1155
+ return false;
1156
+ // `npx skills add` project scope → `.claude/skills/`
1157
+ case "shell-script":
1158
+ return false;
1159
+ // ecc-prune → `.claude/local-plugins/`
1160
+ case "npx-run":
1161
+ return false;
1162
+ case "internal":
1163
+ return false;
1164
+ }
1165
+ }
1166
+ function mergeAssets(previous, current) {
1167
+ if (!previous || previous.length === 0) return [...current];
1168
+ const currentById = new Map(current.map((a) => [a.id, a]));
1169
+ const previousIds = new Set(previous.map((a) => a.id));
1170
+ return [
1171
+ ...previous.map((a) => currentById.get(a.id) ?? a),
1172
+ ...current.filter((a) => !previousIds.has(a.id))
1173
+ ];
1174
+ }
1175
+ function hashContent(content) {
1176
+ return createHash("sha256").update(content, "utf8").digest("hex");
1177
+ }
1178
+ function isHarnessOwned(baseline, key, current) {
1179
+ const recorded = baseline.get(key);
1180
+ return recorded !== void 0 && recorded === hashContent(current);
1181
+ }
1182
+ function mergeExternalFiles(projectDir, previous, current) {
1183
+ const byPath = /* @__PURE__ */ new Map();
1184
+ for (const file of [...previous ?? [], ...current]) byPath.set(file.path, file);
1185
+ return [...byPath.values()].filter((f) => existsSync2(join2(projectDir, f.path)));
1186
+ }
1187
+ function collectSkillHashes(projectDir) {
1188
+ const skillsDir = join2(projectDir, ".claude/skills");
1189
+ return listFilesRecursive(skillsDir).map((rel) => ({
1190
+ path: rel,
1191
+ sha256: hashContent(readFileSync2(join2(skillsDir, rel), "utf8"))
1192
+ }));
1193
+ }
1194
+ function collectPolicyHashes(projectDir, templatesDir) {
1195
+ const out = [];
1196
+ for (const { dir, ext } of POLICY_DIRS) {
1197
+ const targetDir = join2(projectDir, ".claude", dir);
1198
+ const sourceDir = join2(templatesDir, dir);
1199
+ for (const rel of listFilesRecursive(targetDir)) {
1200
+ if (!rel.endsWith(ext)) continue;
1201
+ if (!existsSync2(join2(sourceDir, rel))) continue;
1202
+ out.push({
1203
+ path: `${dir}/${rel}`,
1204
+ sha256: hashContent(readFileSync2(join2(targetDir, rel), "utf8"))
1205
+ });
1206
+ }
1207
+ }
1208
+ return out;
1209
+ }
1210
+ function writeInstallLog(projectDir, log) {
1211
+ const path = join2(projectDir, ".claude", INSTALL_LOG_FILENAME);
1212
+ mkdirSync2(dirname2(path), { recursive: true });
1213
+ writeFileSync(path, `${JSON.stringify(log, null, 2)}
1214
+ `, "utf8");
1215
+ return path;
1216
+ }
1217
+ function readInstallLog(projectDir) {
1218
+ const path = join2(projectDir, ".claude", INSTALL_LOG_FILENAME);
1219
+ if (!existsSync2(path)) return null;
1220
+ try {
1221
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
1222
+ if (Array.isArray(parsed.assets)) {
1223
+ parsed.assets = parsed.assets.map(
1224
+ (a) => a.method === "npm-global" ? { ...a, method: "npm" } : a
1225
+ );
1226
+ }
1227
+ return parsed;
1228
+ } catch {
1229
+ return null;
1230
+ }
1231
+ }
1232
+ function installLogPath(projectDir) {
1233
+ return join2(projectDir, ".claude", INSTALL_LOG_FILENAME);
1234
+ }
1235
+
1236
+ // src/owned-write.ts
1237
+ function createOwnedWriter(projectDir, baseline, now = /* @__PURE__ */ new Date()) {
1238
+ const written = /* @__PURE__ */ new Map();
1239
+ const backedUp = [];
1240
+ const backupPaths = [];
1241
+ return {
1242
+ write(absPath, content) {
1243
+ const rel = relative(projectDir, absPath).split(sep).join("/");
1244
+ const digest = hashContent(content);
1245
+ if (!existsSync3(absPath)) {
1246
+ mkdirSync3(dirname3(absPath), { recursive: true });
1247
+ writeFileSync2(absPath, content);
1248
+ } else {
1249
+ const current = readFileSync3(absPath, "utf8");
1250
+ if (current !== content) {
1251
+ if (!isHarnessOwned(baseline, rel, current)) {
1252
+ backupPaths.push(backupFile(absPath, now));
1253
+ backedUp.push(rel);
1254
+ }
1255
+ writeFileSync2(absPath, content);
1256
+ }
1257
+ }
1258
+ written.set(rel, digest);
1259
+ },
1260
+ result() {
1261
+ return {
1262
+ files: [...written].map(([path, sha256]) => ({ path, sha256 })),
1263
+ backedUp: [...backedUp],
1264
+ backupPaths: [...backupPaths]
1265
+ };
1266
+ }
1267
+ };
1268
+ }
1269
+
1053
1270
  // src/project-claude-merge.ts
1054
1271
  init_esm_shims();
1055
1272
  var TRACK_DISPLAY_NAMES = {
@@ -1144,48 +1361,48 @@ function expandTracks(tracks) {
1144
1361
 
1145
1362
  // src/antigravity/transform.ts
1146
1363
  function runAntigravityTransform(params) {
1147
- const { harnessRoot, projectDir, selectedInternalSkills = [] } = params;
1148
- const rulesFile = writeRules(harnessRoot, projectDir);
1364
+ const { harnessRoot, projectDir, selectedInternalSkills = [], baseline } = params;
1365
+ const writer = createOwnedWriter(projectDir, baseline);
1366
+ const rulesFile = writeRules(harnessRoot, projectDir, writer);
1149
1367
  const skillFiles = [];
1150
1368
  for (const id of selectedInternalSkills) {
1151
- const src = join2(harnessRoot, "templates/skills", id, "SKILL.md");
1152
- if (!existsSync2(src)) {
1369
+ const src = join3(harnessRoot, "templates/skills", id, "SKILL.md");
1370
+ if (!existsSync4(src)) {
1153
1371
  continue;
1154
1372
  }
1155
- const skillDir = join2(projectDir, ".agents", "skills", id);
1156
- ensureDir(skillDir);
1157
- const target = join2(skillDir, "SKILL.md");
1158
- writeFileSync(target, renderBundledSkill(readFileSync2(src, "utf8")));
1373
+ const target = join3(projectDir, ".agents", "skills", id, "SKILL.md");
1374
+ writer.write(target, renderBundledSkill(readFileSync4(src, "utf8")));
1159
1375
  skillFiles.push(target);
1160
1376
  }
1161
- return { rulesFile, skillFiles };
1377
+ return {
1378
+ rulesFile,
1379
+ skillFiles,
1380
+ ownership: writer.result()
1381
+ };
1162
1382
  }
1163
- function writeRules(harnessRoot, projectDir) {
1164
- const claudeMdPath = join2(harnessRoot, "templates/CLAUDE.md");
1165
- const templatePath = join2(harnessRoot, "templates/antigravity/AGENTS.md.template");
1166
- if (!existsSync2(claudeMdPath) || !existsSync2(templatePath)) {
1383
+ function writeRules(harnessRoot, projectDir, writer) {
1384
+ const claudeMdPath = join3(harnessRoot, "templates/CLAUDE.md");
1385
+ const templatePath = join3(harnessRoot, "templates/antigravity/AGENTS.md.template");
1386
+ if (!existsSync4(claudeMdPath) || !existsSync4(templatePath)) {
1167
1387
  return null;
1168
1388
  }
1169
- const claudeMd = readFileSync2(claudeMdPath, "utf8");
1170
- const template = readFileSync2(templatePath, "utf8");
1171
- const rulesDir = join2(projectDir, ".agents", "rules");
1172
- ensureDir(rulesDir);
1173
- const target = join2(rulesDir, "uzys-harness.md");
1389
+ const claudeMd = readFileSync4(claudeMdPath, "utf8");
1390
+ const template = readFileSync4(templatePath, "utf8");
1391
+ const target = join3(projectDir, ".agents", "rules", "uzys-harness.md");
1174
1392
  const rulesOut = renderAgentsMd({
1175
1393
  template,
1176
1394
  claudeMd,
1177
1395
  projectName: basename(projectDir),
1178
1396
  projectContext: renderFillScaffold()
1179
1397
  });
1180
- backupFileIfChanged(target, rulesOut);
1181
- writeFileSync(target, rulesOut);
1398
+ writer.write(target, rulesOut);
1182
1399
  return target;
1183
1400
  }
1184
1401
 
1185
1402
  // src/ci-scaffold.ts
1186
1403
  init_esm_shims();
1187
- import { existsSync as existsSync3 } from "fs";
1188
- import { join as join3 } from "path";
1404
+ import { existsSync as existsSync5 } from "fs";
1405
+ import { join as join4 } from "path";
1189
1406
  var PYTHON_TRACKS = "data|csr-fastapi|full";
1190
1407
  var VARIANTS = [
1191
1408
  {
@@ -1209,12 +1426,12 @@ function installCiScaffold(ctx) {
1209
1426
  for (const v2 of VARIANTS) {
1210
1427
  if (!v2.applies(ctx.tracks)) continue;
1211
1428
  const relTarget = `.github/workflows/${v2.target}`;
1212
- const target = join3(ctx.projectDir, relTarget);
1213
- if (existsSync3(target)) {
1429
+ const target = join4(ctx.projectDir, relTarget);
1430
+ if (existsSync5(target)) {
1214
1431
  report.skippedExisting.push(relTarget);
1215
1432
  continue;
1216
1433
  }
1217
- copyFile(join3(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
1434
+ copyFile(join4(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
1218
1435
  report.written.push(relTarget);
1219
1436
  }
1220
1437
  return report;
@@ -1223,18 +1440,18 @@ function installCiScaffold(ctx) {
1223
1440
  // src/codex/opt-in.ts
1224
1441
  init_esm_shims();
1225
1442
  import { homedir } from "os";
1226
- import { join as join4 } from "path";
1443
+ import { join as join5 } from "path";
1227
1444
 
1228
1445
  // src/codex/trust-entry.ts
1229
1446
  init_esm_shims();
1230
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1231
- import { dirname as dirname2 } from "path";
1447
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1448
+ import { dirname as dirname4 } from "path";
1232
1449
  var TRUST_BLOCK_REGEX = /\[projects\."([^"]+)"\]/g;
1233
1450
  function registerTrustEntry(opts) {
1234
1451
  const { configPath, projectDir } = opts;
1235
1452
  try {
1236
- mkdirSync2(dirname2(configPath), { recursive: true });
1237
- const existing = existsSync4(configPath) ? readFileSync3(configPath, "utf8") : "";
1453
+ mkdirSync4(dirname4(configPath), { recursive: true });
1454
+ const existing = existsSync6(configPath) ? readFileSync5(configPath, "utf8") : "";
1238
1455
  if (hasTrustEntry(existing, projectDir)) {
1239
1456
  return { status: "already-present" };
1240
1457
  }
@@ -1242,7 +1459,7 @@ function registerTrustEntry(opts) {
1242
1459
  [projects."${projectDir}"]
1243
1460
  trust_level = "trusted"
1244
1461
  `;
1245
- writeFileSync2(configPath, existing + block);
1462
+ writeFileSync3(configPath, existing + block);
1246
1463
  return { status: "registered" };
1247
1464
  } catch (e2) {
1248
1465
  return {
@@ -1258,8 +1475,8 @@ function hasTrustEntry(configContent, projectDir) {
1258
1475
 
1259
1476
  // src/codex/opt-in.ts
1260
1477
  function runCodexOptIn(ctx) {
1261
- const codexHome = ctx.codexHome ?? join4(homedir(), ".codex");
1262
- const configPath = join4(codexHome, "config.toml");
1478
+ const codexHome = ctx.codexHome ?? join5(homedir(), ".codex");
1479
+ const configPath = join5(codexHome, "config.toml");
1263
1480
  const result = registerTrustEntry({ configPath, projectDir: ctx.projectDir });
1264
1481
  return {
1265
1482
  trustEntry: {
@@ -1272,8 +1489,8 @@ function runCodexOptIn(ctx) {
1272
1489
 
1273
1490
  // src/codex/transform.ts
1274
1491
  init_esm_shims();
1275
- import { chmodSync, existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1276
- import { basename as basename2, join as join5 } from "path";
1492
+ import { chmodSync, existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1493
+ import { basename as basename2, join as join6 } from "path";
1277
1494
 
1278
1495
  // src/codex/config-toml.ts
1279
1496
  init_esm_shims();
@@ -1341,13 +1558,14 @@ function quoteIfNeeded(name) {
1341
1558
  var HOOK_NAMES = ["session-start"];
1342
1559
  var ENV_VAR_RENAME = /CLAUDE_PROJECT_DIR/g;
1343
1560
  function runCodexTransform(params) {
1344
- const { harnessRoot, projectDir, selectedInternalSkills = [] } = params;
1345
- const claudeMd = readRequired(join5(harnessRoot, "templates/CLAUDE.md"));
1346
- const agentsTemplate = readRequired(join5(harnessRoot, "templates/codex/AGENTS.md.template"));
1347
- const configTemplate = readRequired(join5(harnessRoot, "templates/codex/config.toml.template"));
1561
+ const { harnessRoot, projectDir, selectedInternalSkills = [], baseline } = params;
1562
+ const writer = createOwnedWriter(projectDir, baseline);
1563
+ const claudeMd = readRequired(join6(harnessRoot, "templates/CLAUDE.md"));
1564
+ const agentsTemplate = readRequired(join6(harnessRoot, "templates/codex/AGENTS.md.template"));
1565
+ const configTemplate = readRequired(join6(harnessRoot, "templates/codex/config.toml.template"));
1348
1566
  const projectName = basename2(projectDir);
1349
- const mcp = readOptionalJson(join5(harnessRoot, ".mcp.json"));
1350
- const agentsMdPath = join5(projectDir, "AGENTS.md");
1567
+ const mcp = readOptionalJson(join6(harnessRoot, ".mcp.json"));
1568
+ const agentsMdPath = join6(projectDir, "AGENTS.md");
1351
1569
  ensureDir(projectDir);
1352
1570
  const agentsMdOut = renderAgentsMd({
1353
1571
  template: agentsTemplate,
@@ -1355,11 +1573,9 @@ function runCodexTransform(params) {
1355
1573
  projectName,
1356
1574
  projectContext: renderFillScaffold()
1357
1575
  });
1358
- backupFileIfChanged(agentsMdPath, agentsMdOut);
1359
- writeFileSync3(agentsMdPath, agentsMdOut);
1360
- const configTomlPath = join5(projectDir, ".codex/config.toml");
1361
- ensureDir(join5(projectDir, ".codex"));
1362
- writeFileSync3(
1576
+ writer.write(agentsMdPath, agentsMdOut);
1577
+ const configTomlPath = join6(projectDir, ".codex/config.toml");
1578
+ writer.write(
1363
1579
  configTomlPath,
1364
1580
  renderConfigToml({
1365
1581
  template: configTemplate,
@@ -1368,46 +1584,49 @@ function runCodexTransform(params) {
1368
1584
  mcp
1369
1585
  })
1370
1586
  );
1371
- const hookDir = join5(projectDir, ".codex/hooks");
1372
- ensureDir(hookDir);
1587
+ const hookDir = join6(projectDir, ".codex/hooks");
1373
1588
  const hookFiles = [];
1374
1589
  for (const hook of HOOK_NAMES) {
1375
- const src = join5(harnessRoot, "templates/hooks", `${hook}.sh`);
1376
- if (!existsSync5(src)) {
1590
+ const src = join6(harnessRoot, "templates/hooks", `${hook}.sh`);
1591
+ if (!existsSync7(src)) {
1377
1592
  continue;
1378
1593
  }
1379
- const ported = readFileSync4(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
1380
- const target = join5(hookDir, `${hook}.sh`);
1381
- writeFileSync3(target, ported);
1594
+ const ported = readFileSync6(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
1595
+ const target = join6(hookDir, `${hook}.sh`);
1596
+ writer.write(target, ported);
1382
1597
  chmodSync(target, 493);
1383
1598
  hookFiles.push(target);
1384
1599
  }
1385
1600
  const skillFiles = [];
1386
1601
  for (const id of selectedInternalSkills) {
1387
- const src = join5(harnessRoot, "templates/skills", id, "SKILL.md");
1388
- if (!existsSync5(src)) {
1602
+ const src = join6(harnessRoot, "templates/skills", id, "SKILL.md");
1603
+ if (!existsSync7(src)) {
1389
1604
  continue;
1390
1605
  }
1391
- const skillDir = join5(projectDir, ".agents", "skills", id);
1392
- ensureDir(skillDir);
1393
- const target = join5(skillDir, "SKILL.md");
1394
- writeFileSync3(target, renderBundledSkill(readFileSync4(src, "utf8")));
1606
+ const target = join6(projectDir, ".agents", "skills", id, "SKILL.md");
1607
+ writer.write(target, renderBundledSkill(readFileSync6(src, "utf8")));
1395
1608
  skillFiles.push(target);
1396
1609
  }
1397
- return { agentsMdPath, configTomlPath, hookFiles, skillFiles };
1610
+ return {
1611
+ agentsMdPath,
1612
+ configTomlPath,
1613
+ hookFiles,
1614
+ skillFiles,
1615
+ ownership: writer.result()
1616
+ };
1398
1617
  }
1399
1618
  function readRequired(path) {
1400
- if (!existsSync5(path)) {
1619
+ if (!existsSync7(path)) {
1401
1620
  throw new Error(`Codex transform: required source missing: ${path}`);
1402
1621
  }
1403
- return readFileSync4(path, "utf8");
1622
+ return readFileSync6(path, "utf8");
1404
1623
  }
1405
1624
  function readOptionalJson(path) {
1406
- if (!existsSync5(path)) {
1625
+ if (!existsSync7(path)) {
1407
1626
  return null;
1408
1627
  }
1409
1628
  try {
1410
- return JSON.parse(readFileSync4(path, "utf8"));
1629
+ return JSON.parse(readFileSync6(path, "utf8"));
1411
1630
  } catch {
1412
1631
  return null;
1413
1632
  }
@@ -1415,8 +1634,8 @@ function readOptionalJson(path) {
1415
1634
 
1416
1635
  // src/env-files.ts
1417
1636
  init_esm_shims();
1418
- import { appendFileSync, existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
1419
- import { join as join6 } from "path";
1637
+ import { appendFileSync, existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
1638
+ import { join as join7 } from "path";
1420
1639
  var ENV_EXAMPLE_BODY = `# .env.example \u2014 csr-supabase Track
1421
1640
  # Copy to .env (gitignored) and fill in values: cp .env.example .env
1422
1641
 
@@ -1453,24 +1672,24 @@ function writeEnvExample(projectDir, tracks) {
1453
1672
  if (!tracks.some((t) => ENV_EXAMPLE_TRACKS.includes(t))) {
1454
1673
  return false;
1455
1674
  }
1456
- const path = join6(projectDir, ".env.example");
1457
- if (existsSync6(path)) {
1675
+ const path = join7(projectDir, ".env.example");
1676
+ if (existsSync8(path)) {
1458
1677
  return false;
1459
1678
  }
1460
1679
  writeFileSync4(path, ENV_EXAMPLE_BODY);
1461
1680
  return true;
1462
1681
  }
1463
1682
  function addGitignoreEnv(projectDir) {
1464
- const path = join6(projectDir, ".gitignore");
1465
- if (!existsSync6(path)) {
1683
+ const path = join7(projectDir, ".gitignore");
1684
+ if (!existsSync8(path)) {
1466
1685
  return false;
1467
1686
  }
1468
- const content = readFileSync5(path, "utf8");
1687
+ const content = readFileSync7(path, "utf8");
1469
1688
  if (GITIGNORE_ENV_PATTERN.test(content)) {
1470
1689
  return false;
1471
1690
  }
1472
- const sep = content.endsWith("\n") ? "" : "\n";
1473
- appendFileSync(path, `${sep}
1691
+ const sep2 = content.endsWith("\n") ? "" : "\n";
1692
+ appendFileSync(path, `${sep2}
1474
1693
  # Secret env (auto-added by agent-harness install)
1475
1694
  .env
1476
1695
  `);
@@ -1479,11 +1698,11 @@ function addGitignoreEnv(projectDir) {
1479
1698
  var NPX_SKILLS_AGENT_DIRS = [".factory/", ".goose/"];
1480
1699
  var GITIGNORE_NPX_SKILLS_HEADER = "# npx skills add multi-CLI cache (auto-added by agent-harness)";
1481
1700
  function addGitignoreNpxSkillsAgents(projectDir) {
1482
- const path = join6(projectDir, ".gitignore");
1483
- if (!existsSync6(path)) {
1701
+ const path = join7(projectDir, ".gitignore");
1702
+ if (!existsSync8(path)) {
1484
1703
  return [];
1485
1704
  }
1486
- const content = readFileSync5(path, "utf8");
1705
+ const content = readFileSync7(path, "utf8");
1487
1706
  const missing = NPX_SKILLS_AGENT_DIRS.filter((pattern) => {
1488
1707
  const lineRegex = new RegExp(`^${pattern.replace(/\./g, "\\.").replace(/\//g, "/")}\\s*$`, "m");
1489
1708
  return !lineRegex.test(content);
@@ -1491,25 +1710,25 @@ function addGitignoreNpxSkillsAgents(projectDir) {
1491
1710
  if (missing.length === 0) {
1492
1711
  return [];
1493
1712
  }
1494
- const sep = content.endsWith("\n") ? "" : "\n";
1713
+ const sep2 = content.endsWith("\n") ? "" : "\n";
1495
1714
  const block = [GITIGNORE_NPX_SKILLS_HEADER, ...missing].join("\n");
1496
- appendFileSync(path, `${sep}
1715
+ appendFileSync(path, `${sep2}
1497
1716
  ${block}
1498
1717
  `);
1499
1718
  return [...missing];
1500
1719
  }
1501
1720
  function writeMcpAllowlist(projectDir) {
1502
- const allowlistPath = join6(projectDir, ".mcp-allowlist");
1503
- if (existsSync6(allowlistPath)) {
1721
+ const allowlistPath = join7(projectDir, ".mcp-allowlist");
1722
+ if (existsSync8(allowlistPath)) {
1504
1723
  return null;
1505
1724
  }
1506
- const mcpPath = join6(projectDir, ".mcp.json");
1507
- if (!existsSync6(mcpPath)) {
1725
+ const mcpPath = join7(projectDir, ".mcp.json");
1726
+ if (!existsSync8(mcpPath)) {
1508
1727
  return null;
1509
1728
  }
1510
1729
  let names = [];
1511
1730
  try {
1512
- const parsed = JSON.parse(readFileSync5(mcpPath, "utf8"));
1731
+ const parsed = JSON.parse(readFileSync7(mcpPath, "utf8"));
1513
1732
  names = Object.keys(parsed.mcpServers ?? {}).sort();
1514
1733
  } catch {
1515
1734
  return null;
@@ -1532,9 +1751,9 @@ function writeMcpAllowlist(projectDir) {
1532
1751
  // src/external-installer.ts
1533
1752
  init_esm_shims();
1534
1753
  import { spawnSync } from "child_process";
1535
- import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
1754
+ import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync8 } from "fs";
1536
1755
  import { homedir as homedir2 } from "os";
1537
- import { join as join7 } from "path";
1756
+ import { join as join8 } from "path";
1538
1757
  var DEFAULT_SPAWN_TIMEOUT_MS = 12e4;
1539
1758
  function selectExternalTargets(assets, ctx) {
1540
1759
  const conditionPassed = filterApplicableAssets(assets, ctx).filter(
@@ -1610,8 +1829,8 @@ function installOne(asset, ctx) {
1610
1829
  cwd
1611
1830
  );
1612
1831
  case "shell-script": {
1613
- const scriptPath = join7(ctx.harnessRoot, method.script);
1614
- if (!existsSync7(scriptPath)) {
1832
+ const scriptPath = join8(ctx.harnessRoot, method.script);
1833
+ if (!existsSync9(scriptPath)) {
1615
1834
  return {
1616
1835
  asset,
1617
1836
  ok: false,
@@ -1701,17 +1920,17 @@ function detectVersion(method, spawn) {
1701
1920
  if (at2 <= 0) return void 0;
1702
1921
  const plugin = method.pluginId.slice(0, at2);
1703
1922
  const marketplaceShort = method.pluginId.slice(at2 + 1);
1704
- const cacheBase = join7(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
1705
- if (!existsSync7(cacheBase)) return void 0;
1923
+ const cacheBase = join8(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
1924
+ if (!existsSync9(cacheBase)) return void 0;
1706
1925
  const versions = readdirSync2(cacheBase).filter((v2) => /^\d/.test(v2)).sort();
1707
1926
  return versions.at(-1);
1708
1927
  }
1709
1928
  case "npm": {
1710
1929
  const npmRoot = getNpmGlobalRoot(spawn);
1711
1930
  if (!npmRoot) return void 0;
1712
- const pkgJson = join7(npmRoot, method.pkg, "package.json");
1713
- if (!existsSync7(pkgJson)) return void 0;
1714
- const parsed = JSON.parse(readFileSync6(pkgJson, "utf8"));
1931
+ const pkgJson = join8(npmRoot, method.pkg, "package.json");
1932
+ if (!existsSync9(pkgJson)) return void 0;
1933
+ const parsed = JSON.parse(readFileSync8(pkgJson, "utf8"));
1715
1934
  return parsed.version;
1716
1935
  }
1717
1936
  default:
@@ -1736,156 +1955,9 @@ function getNpmGlobalRoot(spawn) {
1736
1955
  return void 0;
1737
1956
  }
1738
1957
 
1739
- // src/install-log.ts
1740
- init_esm_shims();
1741
- import { createHash } from "crypto";
1742
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
1743
- import { dirname as dirname3, join as join8 } from "path";
1744
- var INSTALL_LOG_FILENAME = ".harness-install.json";
1745
- var INSTALL_LOG_VERSION = 1;
1746
- function buildAssetEntries(report, scope) {
1747
- if (!report) return [];
1748
- return report.attempted.filter((r) => r.ok).map((r) => assetToLogEntry(r.asset, scope, r.version));
1749
- }
1750
- function assetToLogEntry(asset, scope, version) {
1751
- const detail = methodDetail(asset.method);
1752
- const entry = {
1753
- id: asset.id,
1754
- category: asset.category,
1755
- method: asset.method.kind,
1756
- scope,
1757
- detail
1758
- };
1759
- if (version) entry.version = version;
1760
- return entry;
1761
- }
1762
- function methodDetail(method) {
1763
- switch (method.kind) {
1764
- case "plugin":
1765
- return { marketplace: method.marketplace, pluginId: method.pluginId };
1766
- case "skill":
1767
- return { source: method.source, ...method.skill ? { skill: method.skill } : {} };
1768
- case "npm":
1769
- return { pkg: method.pkg };
1770
- case "npx-run":
1771
- return { cmd: method.cmd, args: (method.args ?? []).join(" ") };
1772
- case "shell-script":
1773
- return { script: method.script, args: method.args.join(" ") };
1774
- case "internal":
1775
- return { key: method.key };
1776
- }
1777
- }
1778
- function buildInstallLog(spec, external, scope, rootClaudeMd, previous, claudeDirMovedAside = false, rootFiles = []) {
1779
- const templates = {
1780
- claudeDir: ".claude/",
1781
- ...spec.cli.includes("codex") ? { codexDir: ".codex/" } : {},
1782
- ...spec.cli.includes("opencode") ? { opencodeDir: ".opencode/" } : {},
1783
- ...rootClaudeMd ? { rootClaudeMd } : {}
1784
- };
1785
- const log = {
1786
- schemaVersion: INSTALL_LOG_VERSION,
1787
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1788
- scope,
1789
- spec: {
1790
- tracks: spec.tracks,
1791
- cli: spec.cli
1792
- },
1793
- // 이번 설치가 만든 항목이 이기고, 이번에 안 만든 항목은 이전 값을 그대로 둔다.
1794
- // (예: claude 로 깔고 나중에 codex 만 추가 설치해도 root CLAUDE.md 기록이 살아남는다)
1795
- templates: { ...previous?.templates, ...templates },
1796
- assets: mergeAssets(
1797
- claudeDirMovedAside ? previous?.assets?.filter(survivesClaudeDirRename) : previous?.assets,
1798
- buildAssetEntries(external, scope)
1799
- )
1800
- };
1801
- const mergedRootFiles = mergeRootFiles(previous?.rootFiles, rootFiles);
1802
- if (mergedRootFiles.length > 0) log.rootFiles = mergedRootFiles;
1803
- return log;
1804
- }
1805
- function mergeRootFiles(previous, current) {
1806
- const byPath = /* @__PURE__ */ new Map();
1807
- for (const file of [...previous ?? [], ...current]) {
1808
- const prior = byPath.get(file.path);
1809
- byPath.set(
1810
- file.path,
1811
- prior ? {
1812
- path: file.path,
1813
- change: prior.change === "created" ? "created" : file.change,
1814
- notes: [.../* @__PURE__ */ new Set([...prior.notes, ...file.notes])]
1815
- } : file
1816
- );
1817
- }
1818
- return [...byPath.values()];
1819
- }
1820
- function survivesClaudeDirRename(asset) {
1821
- if (asset.scope === "global") return true;
1822
- switch (asset.method) {
1823
- case "plugin":
1824
- return true;
1825
- // `~/.claude/plugins/cache` — 프로젝트 밖
1826
- case "npm":
1827
- return true;
1828
- // `node_modules/`
1829
- case "skill":
1830
- return false;
1831
- // `npx skills add` project scope → `.claude/skills/`
1832
- case "shell-script":
1833
- return false;
1834
- // ecc-prune → `.claude/local-plugins/`
1835
- case "npx-run":
1836
- return false;
1837
- case "internal":
1838
- return false;
1839
- }
1840
- }
1841
- function mergeAssets(previous, current) {
1842
- if (!previous || previous.length === 0) return [...current];
1843
- const currentById = new Map(current.map((a) => [a.id, a]));
1844
- const previousIds = new Set(previous.map((a) => a.id));
1845
- return [
1846
- ...previous.map((a) => currentById.get(a.id) ?? a),
1847
- ...current.filter((a) => !previousIds.has(a.id))
1848
- ];
1849
- }
1850
- function hashContent(content) {
1851
- return createHash("sha256").update(content, "utf8").digest("hex");
1852
- }
1853
- function collectSkillHashes(projectDir) {
1854
- const skillsDir = join8(projectDir, ".claude/skills");
1855
- return listFilesRecursive(skillsDir).map((rel) => ({
1856
- path: rel,
1857
- sha256: hashContent(readFileSync7(join8(skillsDir, rel), "utf8"))
1858
- }));
1859
- }
1860
- function writeInstallLog(projectDir, log) {
1861
- const path = join8(projectDir, ".claude", INSTALL_LOG_FILENAME);
1862
- mkdirSync3(dirname3(path), { recursive: true });
1863
- writeFileSync5(path, `${JSON.stringify(log, null, 2)}
1864
- `, "utf8");
1865
- return path;
1866
- }
1867
- function readInstallLog(projectDir) {
1868
- const path = join8(projectDir, ".claude", INSTALL_LOG_FILENAME);
1869
- if (!existsSync8(path)) return null;
1870
- try {
1871
- const parsed = JSON.parse(readFileSync7(path, "utf8"));
1872
- if (Array.isArray(parsed.assets)) {
1873
- parsed.assets = parsed.assets.map(
1874
- (a) => a.method === "npm-global" ? { ...a, method: "npm" } : a
1875
- );
1876
- }
1877
- return parsed;
1878
- } catch {
1879
- return null;
1880
- }
1881
- }
1882
- function installLogPath(projectDir) {
1883
- return join8(projectDir, ".claude", INSTALL_LOG_FILENAME);
1884
- }
1885
-
1886
1958
  // src/mcp-merge.ts
1887
1959
  init_esm_shims();
1888
- import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
1960
+ import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
1889
1961
  function parseTrackMcpMap(raw) {
1890
1962
  const rows = [];
1891
1963
  for (const line of raw.split(/\r?\n/)) {
@@ -1936,15 +2008,15 @@ function mergeMcpServers(base, rows, tracks) {
1936
2008
  return out;
1937
2009
  }
1938
2010
  function composeMcpJson(opts) {
1939
- const base = JSON.parse(readFileSync8(opts.templateMcpPath, "utf8"));
1940
- const merged = opts.existingPath && existsSync9(opts.existingPath) ? mergeUserBase(base, opts.existingPath) : base;
1941
- const mapRaw = existsSync9(opts.trackMapPath) ? readFileSync8(opts.trackMapPath, "utf8") : "";
2011
+ const base = JSON.parse(readFileSync9(opts.templateMcpPath, "utf8"));
2012
+ const merged = opts.existingPath && existsSync10(opts.existingPath) ? mergeUserBase(base, opts.existingPath) : base;
2013
+ const mapRaw = existsSync10(opts.trackMapPath) ? readFileSync9(opts.trackMapPath, "utf8") : "";
1942
2014
  const rows = parseTrackMcpMap(mapRaw);
1943
2015
  return mergeMcpServers(merged, rows, opts.tracks);
1944
2016
  }
1945
2017
  function mergeUserBase(base, existingPath) {
1946
2018
  try {
1947
- const existing = JSON.parse(readFileSync8(existingPath, "utf8"));
2019
+ const existing = JSON.parse(readFileSync9(existingPath, "utf8"));
1948
2020
  return {
1949
2021
  ...base,
1950
2022
  mcpServers: { ...base.mcpServers, ...existing.mcpServers }
@@ -1954,13 +2026,13 @@ function mergeUserBase(base, existingPath) {
1954
2026
  }
1955
2027
  }
1956
2028
  function writeMcpJson(path, mcp) {
1957
- writeFileSync6(path, `${JSON.stringify(mcp, null, 2)}
2029
+ writeFileSync5(path, `${JSON.stringify(mcp, null, 2)}
1958
2030
  `);
1959
2031
  }
1960
2032
 
1961
2033
  // src/opencode/transform.ts
1962
2034
  init_esm_shims();
1963
- import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
2035
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
1964
2036
  import { basename as basename3, join as join9 } from "path";
1965
2037
 
1966
2038
  // src/opencode/agents-md.ts
@@ -2061,7 +2133,8 @@ function parseTemplate(template) {
2061
2133
 
2062
2134
  // src/opencode/transform.ts
2063
2135
  function runOpencodeTransform(params) {
2064
- const { harnessRoot, projectDir, selectedInternalSkills = [] } = params;
2136
+ const { harnessRoot, projectDir, selectedInternalSkills = [], baseline } = params;
2137
+ const writer = createOwnedWriter(projectDir, baseline);
2065
2138
  const claudeMd = readRequired2(join9(harnessRoot, "templates/CLAUDE.md"));
2066
2139
  const agentsTemplate = readRequired2(join9(harnessRoot, "templates/opencode/AGENTS.md.template"));
2067
2140
  const opencodeTemplate = readRequired2(
@@ -2077,40 +2150,41 @@ function runOpencodeTransform(params) {
2077
2150
  projectName,
2078
2151
  projectContext: renderFillScaffold()
2079
2152
  });
2080
- backupFileIfChanged(agentsMdPath, agentsMdOut);
2081
- writeFileSync7(agentsMdPath, agentsMdOut);
2153
+ writer.write(agentsMdPath, agentsMdOut);
2082
2154
  const opencodeJsonPath = join9(projectDir, "opencode.json");
2083
- writeFileSync7(opencodeJsonPath, renderOpencodeJson({ template: opencodeTemplate, mcp }));
2155
+ writer.write(opencodeJsonPath, renderOpencodeJson({ template: opencodeTemplate, mcp }));
2084
2156
  const cmdDir = join9(projectDir, ".opencode/commands");
2085
2157
  ensureDir(cmdDir);
2086
2158
  const commandFiles = [];
2087
2159
  for (const id of selectedInternalSkills) {
2088
2160
  const src = join9(harnessRoot, "templates/skills", id, "SKILL.md");
2089
- if (!existsSync10(src)) {
2161
+ if (!existsSync11(src)) {
2090
2162
  continue;
2091
2163
  }
2092
2164
  const target = join9(cmdDir, `${id}.md`);
2093
- const shellDependent = existsSync10(join9(harnessRoot, "templates/skills", id, "scripts"));
2094
- writeFileSync7(
2095
- target,
2096
- renderCommandFromSkill(readFileSync9(src, "utf8"), id, { shellDependent })
2097
- );
2165
+ const shellDependent = existsSync11(join9(harnessRoot, "templates/skills", id, "scripts"));
2166
+ writer.write(target, renderCommandFromSkill(readFileSync10(src, "utf8"), id, { shellDependent }));
2098
2167
  commandFiles.push(target);
2099
2168
  }
2100
- return { agentsMdPath, opencodeJsonPath, commandFiles };
2169
+ return {
2170
+ agentsMdPath,
2171
+ opencodeJsonPath,
2172
+ commandFiles,
2173
+ ownership: writer.result()
2174
+ };
2101
2175
  }
2102
2176
  function readRequired2(path) {
2103
- if (!existsSync10(path)) {
2177
+ if (!existsSync11(path)) {
2104
2178
  throw new Error(`OpenCode transform: required source missing: ${path}`);
2105
2179
  }
2106
- return readFileSync9(path, "utf8");
2180
+ return readFileSync10(path, "utf8");
2107
2181
  }
2108
2182
  function readOptionalJson2(path) {
2109
- if (!existsSync10(path)) {
2183
+ if (!existsSync11(path)) {
2110
2184
  return null;
2111
2185
  }
2112
2186
  try {
2113
- return JSON.parse(readFileSync9(path, "utf8"));
2187
+ return JSON.parse(readFileSync10(path, "utf8"));
2114
2188
  } catch {
2115
2189
  return null;
2116
2190
  }
@@ -2144,14 +2218,14 @@ function addPreToolUseHook(settings, matcher, command) {
2144
2218
  init_esm_shims();
2145
2219
  import {
2146
2220
  copyFileSync as copyFileSync2,
2147
- existsSync as existsSync11,
2148
- mkdirSync as mkdirSync4,
2221
+ existsSync as existsSync12,
2222
+ mkdirSync as mkdirSync5,
2149
2223
  readdirSync as readdirSync3,
2150
- readFileSync as readFileSync10,
2224
+ readFileSync as readFileSync11,
2151
2225
  unlinkSync,
2152
- writeFileSync as writeFileSync8
2226
+ writeFileSync as writeFileSync6
2153
2227
  } from "fs";
2154
- import { dirname as dirname4, join as join10 } from "path";
2228
+ import { dirname as dirname5, join as join10 } from "path";
2155
2229
  function buildUpdateSpec(projectDir, tracks) {
2156
2230
  return {
2157
2231
  tracks: [...tracks],
@@ -2167,38 +2241,21 @@ function runUpdateMode(projectDir, templatesDir) {
2167
2241
  pruned: {},
2168
2242
  staleHookRefs: [],
2169
2243
  claudeMdUpdated: false,
2170
- skillsBackedUp: []
2244
+ skillsBackedUp: [],
2245
+ policyBackedUp: []
2171
2246
  };
2172
- const targets = [
2173
- {
2174
- target: join10(claudeDir, "rules"),
2175
- source: join10(templatesDir, "rules"),
2176
- pattern: ".md",
2177
- label: ".claude/rules"
2178
- },
2179
- {
2180
- target: join10(claudeDir, "agents"),
2181
- source: join10(templatesDir, "agents"),
2182
- pattern: ".md",
2183
- label: ".claude/agents"
2184
- },
2185
- {
2186
- target: join10(claudeDir, "commands/uzys"),
2187
- source: join10(templatesDir, "commands/uzys"),
2188
- pattern: ".md",
2189
- label: ".claude/commands/uzys"
2190
- },
2191
- {
2192
- target: join10(claudeDir, "hooks"),
2193
- source: join10(templatesDir, "hooks"),
2194
- pattern: ".sh",
2195
- label: ".claude/hooks"
2196
- }
2197
- ];
2198
- for (const t of targets) {
2199
- report.updated[t.label] = updateDir(t.target, t.source, t.pattern);
2200
- report.pruned[t.label] = pruneOrphans(t.target, t.source, t.pattern);
2201
- }
2247
+ const policyBase = policyBaseline(projectDir);
2248
+ for (const { dir, ext } of POLICY_DIRS) {
2249
+ const target = join10(claudeDir, dir);
2250
+ const source = join10(templatesDir, dir);
2251
+ const label = `.claude/${dir}`;
2252
+ const ctx = { prefix: dir, baseline: policyBase };
2253
+ const synced = updateDir(target, source, ext, ctx);
2254
+ report.updated[label] = synced.updated;
2255
+ report.policyBackedUp.push(...synced.backedUp);
2256
+ report.pruned[label] = pruneOrphans(target, source, ext, ctx);
2257
+ }
2258
+ refreshPolicyBaseline(projectDir, templatesDir);
2202
2259
  const skillSync = syncSkills(
2203
2260
  join10(claudeDir, "skills"),
2204
2261
  join10(templatesDir, "skills"),
@@ -2209,55 +2266,66 @@ function runUpdateMode(projectDir, templatesDir) {
2209
2266
  refreshSkillBaseline(projectDir);
2210
2267
  const claudeMd = join10(claudeDir, "CLAUDE.md");
2211
2268
  const templateMd = join10(templatesDir, "CLAUDE.md");
2212
- if (existsSync11(claudeMd) && existsSync11(templateMd)) {
2269
+ if (existsSync12(claudeMd) && existsSync12(templateMd)) {
2213
2270
  copyFileSync2(templateMd, claudeMd);
2214
2271
  report.claudeMdUpdated = true;
2215
2272
  }
2216
2273
  const settingsPath = join10(claudeDir, "settings.json");
2217
- if (existsSync11(settingsPath)) {
2274
+ if (existsSync12(settingsPath)) {
2218
2275
  report.staleHookRefs = cleanStaleHookRefs(settingsPath, join10(claudeDir, "hooks"));
2219
2276
  }
2220
2277
  return report;
2221
2278
  }
2222
- function updateDir(target, source, ext) {
2223
- if (!existsSync11(target) || !existsSync11(source)) return 0;
2224
- let count = 0;
2279
+ function isHarnessOwned2(ctx, rel, current) {
2280
+ const recorded = ctx.baseline.get(`${ctx.prefix}/${rel}`);
2281
+ return recorded !== void 0 && recorded === hashContent(current);
2282
+ }
2283
+ function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2284
+ if (!existsSync12(target) || !existsSync12(source)) return { updated: 0, backedUp: [] };
2285
+ let updated = 0;
2286
+ const backedUp = [];
2225
2287
  for (const file of readdirSync3(target)) {
2226
2288
  if (!file.endsWith(ext)) continue;
2227
2289
  const targetFile = join10(target, file);
2228
2290
  const sourceFile = join10(source, file);
2229
- if (existsSync11(sourceFile)) {
2230
- copyFileSync2(sourceFile, targetFile);
2231
- count++;
2291
+ if (!existsSync12(sourceFile)) continue;
2292
+ const next = readFileSync11(sourceFile, "utf8");
2293
+ const current = readFileSync11(targetFile, "utf8");
2294
+ if (current === next) continue;
2295
+ if (!isHarnessOwned2(ctx, file, current)) {
2296
+ backupFile(targetFile, now);
2297
+ backedUp.push(`${ctx.prefix}/${file}`);
2232
2298
  }
2299
+ writeFileSync6(targetFile, next);
2300
+ updated++;
2233
2301
  }
2234
- return count;
2302
+ return { updated, backedUp };
2235
2303
  }
2236
2304
  function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Date()) {
2237
- if (!existsSync11(targetDir) || !existsSync11(sourceDir)) return { updated: 0, backedUp: [] };
2305
+ if (!existsSync12(targetDir) || !existsSync12(sourceDir)) return { updated: 0, backedUp: [] };
2238
2306
  let updated = 0;
2239
2307
  const backedUp = [];
2240
2308
  for (const skill of readdirSync3(sourceDir, { withFileTypes: true })) {
2241
2309
  if (!skill.isDirectory()) continue;
2242
2310
  const targetSkill = join10(targetDir, skill.name);
2243
- if (!existsSync11(targetSkill)) continue;
2311
+ if (!existsSync12(targetSkill)) continue;
2244
2312
  for (const rel of listFilesRecursive(join10(sourceDir, skill.name))) {
2245
2313
  const targetFile = join10(targetSkill, rel);
2246
- const next = readFileSync10(join10(sourceDir, skill.name, rel), "utf8");
2247
- if (!existsSync11(targetFile)) {
2248
- mkdirSync4(dirname4(targetFile), { recursive: true });
2249
- writeFileSync8(targetFile, next);
2314
+ const next = readFileSync11(join10(sourceDir, skill.name, rel), "utf8");
2315
+ if (!existsSync12(targetFile)) {
2316
+ mkdirSync5(dirname5(targetFile), { recursive: true });
2317
+ writeFileSync6(targetFile, next);
2250
2318
  updated++;
2251
2319
  continue;
2252
2320
  }
2253
- const current = readFileSync10(targetFile, "utf8");
2321
+ const current = readFileSync11(targetFile, "utf8");
2254
2322
  if (current === next) continue;
2255
2323
  const recorded = baseline.get(`${skill.name}/${rel}`);
2256
2324
  if (recorded === void 0 || hashContent(current) !== recorded) {
2257
2325
  backupFile(targetFile, now);
2258
2326
  backedUp.push(`${skill.name}/${rel}`);
2259
2327
  }
2260
- writeFileSync8(targetFile, next);
2328
+ writeFileSync6(targetFile, next);
2261
2329
  updated++;
2262
2330
  }
2263
2331
  }
@@ -2267,6 +2335,22 @@ function skillBaseline(projectDir) {
2267
2335
  const log = readInstallLog(projectDir);
2268
2336
  return new Map((log?.skillFiles ?? []).map((f) => [f.path, f.sha256]));
2269
2337
  }
2338
+ function policyBaseline(projectDir) {
2339
+ const log = readInstallLog(projectDir);
2340
+ return new Map((log?.policyFiles ?? []).map((f) => [f.path, f.sha256]));
2341
+ }
2342
+ function refreshPolicyBaseline(projectDir, templatesDir) {
2343
+ const log = readInstallLog(projectDir);
2344
+ if (!log) return;
2345
+ const policyFiles = collectPolicyHashes(projectDir, templatesDir);
2346
+ const next = { ...log };
2347
+ if (policyFiles.length > 0) next.policyFiles = policyFiles;
2348
+ else delete next.policyFiles;
2349
+ try {
2350
+ writeInstallLog(projectDir, next);
2351
+ } catch {
2352
+ }
2353
+ }
2270
2354
  function refreshSkillBaseline(projectDir) {
2271
2355
  const log = readInstallLog(projectDir);
2272
2356
  if (!log) return;
@@ -2279,15 +2363,18 @@ function refreshSkillBaseline(projectDir) {
2279
2363
  } catch {
2280
2364
  }
2281
2365
  }
2282
- function pruneOrphans(target, source, ext) {
2283
- if (!existsSync11(target) || !existsSync11(source)) return [];
2366
+ function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2367
+ if (!existsSync12(target) || !existsSync12(source)) return [];
2284
2368
  const removed = [];
2285
2369
  for (const file of readdirSync3(target)) {
2286
2370
  if (!file.endsWith(ext)) continue;
2287
2371
  const sourceFile = join10(source, file);
2288
- if (!existsSync11(sourceFile)) {
2372
+ if (!existsSync12(sourceFile)) {
2289
2373
  const targetFile = join10(target, file);
2374
+ if (!ctx.baseline.has(`${ctx.prefix}/${file}`)) continue;
2290
2375
  try {
2376
+ const current = readFileSync11(targetFile, "utf8");
2377
+ if (!isHarnessOwned2(ctx, file, current)) backupFile(targetFile, now);
2291
2378
  unlinkSync(targetFile);
2292
2379
  removed.push(file);
2293
2380
  } catch {
@@ -2299,7 +2386,7 @@ function pruneOrphans(target, source, ext) {
2299
2386
  function cleanStaleHookRefs(settingsPath, hooksDir) {
2300
2387
  let settings;
2301
2388
  try {
2302
- settings = JSON.parse(readFileSync10(settingsPath, "utf8"));
2389
+ settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
2303
2390
  } catch {
2304
2391
  return [];
2305
2392
  }
@@ -2318,7 +2405,7 @@ function cleanStaleHookRefs(settingsPath, hooksDir) {
2318
2405
  }
2319
2406
  if (removed.length > 0) {
2320
2407
  const next = { ...settings, hooks: cleanedHooks };
2321
- writeFileSync8(settingsPath, `${JSON.stringify(next, null, 2)}
2408
+ writeFileSync6(settingsPath, `${JSON.stringify(next, null, 2)}
2322
2409
  `);
2323
2410
  }
2324
2411
  return removed;
@@ -2327,7 +2414,7 @@ function keepHookRef(hook, hooksDir, removed) {
2327
2414
  const refMatch = (hook?.command ?? "").match(/\/\.claude\/hooks\/([^"\s/]+\.sh)/);
2328
2415
  if (!refMatch?.[1]) return true;
2329
2416
  const fname = refMatch[1];
2330
- const exists = existsSync11(join10(hooksDir, fname));
2417
+ const exists = existsSync12(join10(hooksDir, fname));
2331
2418
  if (!exists && !removed.includes(fname)) removed.push(fname);
2332
2419
  return exists;
2333
2420
  }
@@ -2340,11 +2427,11 @@ function runInstall(ctx) {
2340
2427
  const { harnessRoot, projectDir, spec } = ctx;
2341
2428
  const mode = ctx.mode ?? "fresh";
2342
2429
  const templatesDir = join11(harnessRoot, "templates");
2343
- if (!existsSync12(templatesDir)) {
2430
+ if (!existsSync13(templatesDir)) {
2344
2431
  throw new Error(`Templates dir not found: ${templatesDir}`);
2345
2432
  }
2346
2433
  const claudeDir = join11(projectDir, ".claude");
2347
- if (mode === "update" && !existsSync12(claudeDir)) {
2434
+ if (mode === "update" && !existsSync13(claudeDir)) {
2348
2435
  throw new Error(`Update mode requires existing .claude/ at ${claudeDir}`);
2349
2436
  }
2350
2437
  const previousLog = readInstallLog(projectDir);
@@ -2353,13 +2440,21 @@ function runInstall(ctx) {
2353
2440
  return runUpdateInstall(ctx, templatesDir, backupPath);
2354
2441
  }
2355
2442
  const manifestSpec = buildManifestSpec(spec);
2356
- const base = spec.cli.includes("claude") ? installClaudeBaseline(manifestSpec, projectDir, templatesDir) : emptyClaudeBaseline();
2443
+ const policyBase = new Map((previousLog?.policyFiles ?? []).map((f) => [f.path, f.sha256]));
2444
+ const base = spec.cli.includes("claude") ? installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase) : emptyClaudeBaseline();
2357
2445
  const mcpResult = composeAndWriteMcp(harnessRoot, projectDir, spec);
2358
2446
  const ciScaffold = isAssetSelected("ci-scaffold", {
2359
2447
  tracks: spec.tracks,
2360
2448
  options: spec.options,
2361
2449
  ...spec.userOverride ? { userOverride: spec.userOverride } : {}
2362
2450
  }) ? installCiScaffold({ harnessRoot, projectDir, tracks: spec.tracks }) : null;
2451
+ const { externalFiles, externalBackups, ...cliTransforms } = runCliTransforms(
2452
+ spec,
2453
+ harnessRoot,
2454
+ projectDir,
2455
+ manifestSpec.selectedInternalSkills,
2456
+ previousLog?.externalFiles ?? []
2457
+ );
2363
2458
  const baseline = {
2364
2459
  filesCopied: base.filesCopied,
2365
2460
  dirsCopied: base.dirsCopied,
@@ -2367,20 +2462,23 @@ function runInstall(ctx) {
2367
2462
  backup: backupPath,
2368
2463
  installedTracks: [...spec.tracks].sort(),
2369
2464
  mcpServers: Object.keys(mcpResult.mcpServers).sort(),
2370
- ...runCliTransforms(spec, harnessRoot, projectDir, manifestSpec.selectedInternalSkills),
2465
+ ...cliTransforms,
2371
2466
  ciScaffold,
2372
2467
  updateMode: null,
2373
2468
  mode,
2374
2469
  envFiles: writeEnvironmentFiles(projectDir, spec.tracks),
2375
2470
  categories: base.categories,
2376
2471
  rootClaudeMd: base.rootClaudeMd,
2377
- backups: base.backups
2472
+ // 외부 CLI 백업도 같은 줄에 노출한다 — 백업이 화면에 안 보이면 사용자는 자기 편집분이
2473
+ // 어디 갔는지 알 수 없고, 그러면 백업은 있어도 없는 것과 같다 (ADR-046/047 과 같은 이유).
2474
+ backups: [...base.backups, ...externalBackups]
2378
2475
  };
2379
2476
  ctx.onProgress?.({ type: "baseline-complete", baseline });
2380
2477
  const external = runExternalPhase(ctx);
2381
2478
  const karpathyHook = wireKarpathyHook(spec, external, harnessRoot, projectDir);
2382
2479
  writeInstallLogSafe(
2383
2480
  ctx,
2481
+ externalFiles,
2384
2482
  external,
2385
2483
  base.rootClaudeMdLog,
2386
2484
  previousLog,
@@ -2453,7 +2551,21 @@ function emptyClaudeBaseline() {
2453
2551
  backups: []
2454
2552
  };
2455
2553
  }
2456
- function installClaudeBaseline(manifestSpec, projectDir, templatesDir) {
2554
+ function backupEditedPolicyFile(entryTarget, target, source, baseline) {
2555
+ const prefix = ".claude/";
2556
+ if (!entryTarget.startsWith(prefix)) return null;
2557
+ const rel = entryTarget.slice(prefix.length);
2558
+ if (!POLICY_DIRS.some(({ dir, ext }) => rel.startsWith(`${dir}/`) && rel.endsWith(ext))) {
2559
+ return null;
2560
+ }
2561
+ if (!existsSync13(target)) return null;
2562
+ const current = readFileSync12(target, "utf-8");
2563
+ if (current === readFileSync12(source, "utf-8")) return null;
2564
+ const recorded = baseline.get(rel);
2565
+ if (recorded !== void 0 && recorded === hashContent(current)) return null;
2566
+ return backupFile(target);
2567
+ }
2568
+ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, policyBase) {
2457
2569
  ensureProjectSkeleton(projectDir);
2458
2570
  const result = emptyClaudeBaseline();
2459
2571
  const manifest = buildManifest(manifestSpec);
@@ -2463,13 +2575,18 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir) {
2463
2575
  }
2464
2576
  const source = join11(templatesDir, entry.source);
2465
2577
  const target = join11(projectDir, entry.target);
2466
- if (!existsSync12(source)) {
2578
+ if (!existsSync13(source)) {
2467
2579
  result.skipped += 1;
2468
2580
  continue;
2469
2581
  }
2470
2582
  if (entry.type === "file") {
2471
2583
  if (entry.target === ".claude/settings.json") {
2472
- const backup = backupFileIfChanged(target, readFileSync11(source, "utf-8"));
2584
+ const backup = backupFileIfChanged(target, readFileSync12(source, "utf-8"));
2585
+ if (backup) {
2586
+ result.backups.push(backup);
2587
+ }
2588
+ } else {
2589
+ const backup = backupEditedPolicyFile(entry.target, target, source, policyBase);
2473
2590
  if (backup) {
2474
2591
  result.backups.push(backup);
2475
2592
  }
@@ -2483,7 +2600,7 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir) {
2483
2600
  accumulateCategory(result.categories, entry);
2484
2601
  }
2485
2602
  const hookDir = join11(projectDir, ".claude/hooks");
2486
- if (existsSync12(hookDir)) {
2603
+ if (existsSync13(hookDir)) {
2487
2604
  chmodHooksSync(hookDir);
2488
2605
  }
2489
2606
  writeInstalledTracks(projectDir, manifestSpec.tracks);
@@ -2504,15 +2621,27 @@ function writeEnvironmentFiles(projectDir, tracks) {
2504
2621
  gitignoreNpxSkillsAdded: addGitignoreNpxSkillsAgents(projectDir)
2505
2622
  };
2506
2623
  }
2507
- function runCliTransforms(spec, harnessRoot, projectDir, selectedInternalSkills) {
2624
+ function runCliTransforms(spec, harnessRoot, projectDir, selectedInternalSkills, previousExternal) {
2625
+ const baseline = new Map(previousExternal.map((f) => [f.path, f.sha256]));
2626
+ const externalFiles = [];
2627
+ const externalBackups = [];
2628
+ const absorb = (report) => {
2629
+ for (const f of report.ownership.files) {
2630
+ baseline.set(f.path, f.sha256);
2631
+ externalFiles.push(f);
2632
+ }
2633
+ externalBackups.push(...report.ownership.backupPaths);
2634
+ };
2508
2635
  let codex = null;
2509
2636
  let codexOptIn = null;
2510
2637
  if (spec.cli.includes("codex")) {
2511
2638
  codex = runCodexTransform({
2512
2639
  harnessRoot,
2513
2640
  projectDir,
2514
- selectedInternalSkills
2641
+ selectedInternalSkills,
2642
+ baseline
2515
2643
  });
2644
+ absorb(codex);
2516
2645
  const installScope = spec.scope ?? "project";
2517
2646
  if (installScope === "global" && spec.options.withCodexTrust) {
2518
2647
  codexOptIn = runCodexOptIn({ projectDir });
@@ -2520,17 +2649,20 @@ function runCliTransforms(spec, harnessRoot, projectDir, selectedInternalSkills)
2520
2649
  }
2521
2650
  let opencode = null;
2522
2651
  if (spec.cli.includes("opencode")) {
2523
- opencode = runOpencodeTransform({ harnessRoot, projectDir, selectedInternalSkills });
2652
+ opencode = runOpencodeTransform({ harnessRoot, projectDir, selectedInternalSkills, baseline });
2653
+ absorb(opencode);
2524
2654
  }
2525
2655
  let antigravity = null;
2526
2656
  if (spec.cli.includes("antigravity")) {
2527
2657
  antigravity = runAntigravityTransform({
2528
2658
  harnessRoot,
2529
2659
  projectDir,
2530
- selectedInternalSkills
2660
+ selectedInternalSkills,
2661
+ baseline
2531
2662
  });
2663
+ absorb(antigravity);
2532
2664
  }
2533
- return { codex, codexOptIn, opencode, antigravity };
2665
+ return { codex, codexOptIn, opencode, antigravity, externalFiles, externalBackups };
2534
2666
  }
2535
2667
  function runExternalPhase(ctx) {
2536
2668
  if (ctx.runExternal === null) {
@@ -2568,7 +2700,7 @@ function runExternalPhase(ctx) {
2568
2700
  ctx.onProgress?.({ type: "external-complete", report: external });
2569
2701
  return external;
2570
2702
  }
2571
- function writeInstallLogSafe(ctx, external, rootClaudeMdLog, previousLog, claudeDirMovedAside, rootFiles) {
2703
+ function writeInstallLogSafe(ctx, externalFiles, external, rootClaudeMdLog, previousLog, claudeDirMovedAside, rootFiles) {
2572
2704
  try {
2573
2705
  const log = buildInstallLog(
2574
2706
  ctx.spec,
@@ -2580,7 +2712,14 @@ function writeInstallLogSafe(ctx, external, rootClaudeMdLog, previousLog, claude
2580
2712
  rootFiles
2581
2713
  );
2582
2714
  const skillFiles = collectSkillHashes(ctx.projectDir);
2583
- writeInstallLog(ctx.projectDir, skillFiles.length > 0 ? { ...log, skillFiles } : log);
2715
+ const policyFiles = collectPolicyHashes(ctx.projectDir, join11(ctx.harnessRoot, "templates"));
2716
+ const merged = mergeExternalFiles(ctx.projectDir, previousLog?.externalFiles, externalFiles);
2717
+ writeInstallLog(ctx.projectDir, {
2718
+ ...log,
2719
+ ...skillFiles.length > 0 ? { skillFiles } : {},
2720
+ ...policyFiles.length > 0 ? { policyFiles } : {},
2721
+ ...merged.length > 0 ? { externalFiles: merged } : {}
2722
+ });
2584
2723
  } catch (e2) {
2585
2724
  ctx.onProgress?.({
2586
2725
  type: "install-log-error",
@@ -2605,7 +2744,7 @@ function wireKarpathyHook(spec, external, harnessRoot, projectDir) {
2605
2744
  const sourceHook = join11(harnessRoot, "templates/hooks/karpathy-gate.sh");
2606
2745
  const targetHook = join11(projectDir, KARPATHY_HOOK_RELPATH);
2607
2746
  let hookScriptCopied = false;
2608
- if (existsSync12(sourceHook)) {
2747
+ if (existsSync13(sourceHook)) {
2609
2748
  copyFile(sourceHook, targetHook);
2610
2749
  try {
2611
2750
  chmodSync2(targetHook, 493);
@@ -2615,8 +2754,8 @@ function wireKarpathyHook(spec, external, harnessRoot, projectDir) {
2615
2754
  }
2616
2755
  const settingsPath = join11(projectDir, ".claude/settings.json");
2617
2756
  let settingsUpdated = false;
2618
- if (existsSync12(settingsPath)) {
2619
- const raw = readFileSync11(settingsPath, "utf8");
2757
+ if (existsSync13(settingsPath)) {
2758
+ const raw = readFileSync12(settingsPath, "utf8");
2620
2759
  let before;
2621
2760
  try {
2622
2761
  before = JSON.parse(raw);
@@ -2627,7 +2766,7 @@ function wireKarpathyHook(spec, external, harnessRoot, projectDir) {
2627
2766
  const beforeStr = JSON.stringify(before);
2628
2767
  const afterStr = JSON.stringify(after);
2629
2768
  if (beforeStr !== afterStr) {
2630
- writeFileSync9(settingsPath, `${JSON.stringify(after, null, 2)}
2769
+ writeFileSync7(settingsPath, `${JSON.stringify(after, null, 2)}
2631
2770
  `);
2632
2771
  settingsUpdated = true;
2633
2772
  }
@@ -2636,7 +2775,7 @@ function wireKarpathyHook(spec, external, harnessRoot, projectDir) {
2636
2775
  }
2637
2776
  function composeAndWriteMcp(harnessRoot, projectDir, spec) {
2638
2777
  const mcpPath = join11(projectDir, ".mcp.json");
2639
- const created = !existsSync12(mcpPath);
2778
+ const created = !existsSync13(mcpPath);
2640
2779
  const composed = composeMcpJson({
2641
2780
  templateMcpPath: join11(harnessRoot, "templates/mcp.json"),
2642
2781
  trackMapPath: join11(harnessRoot, "templates/track-mcp-map.tsv"),
@@ -2700,16 +2839,16 @@ function accumulateCategory(cats, entry) {
2700
2839
  }
2701
2840
  function writeInstalledTracks(projectDir, tracks) {
2702
2841
  const path = join11(projectDir, ".claude/.installed-tracks");
2703
- mkdirSync5(dirname5(path), { recursive: true });
2842
+ mkdirSync6(dirname6(path), { recursive: true });
2704
2843
  const sorted = [...new Set(tracks)].sort().join("\n");
2705
- writeFileSync9(path, `${sorted}
2844
+ writeFileSync7(path, `${sorted}
2706
2845
  `);
2707
2846
  }
2708
2847
  function writeRootClaudeMd(projectDir, tracks) {
2709
2848
  const content = mergeProjectClaude(tracks, { projectName: basename4(projectDir) });
2710
2849
  const target = join11(projectDir, "CLAUDE.md");
2711
2850
  const backup = backupFileIfChanged(target, content);
2712
- writeFileSync9(target, content);
2851
+ writeFileSync7(target, content);
2713
2852
  return { content, backup };
2714
2853
  }
2715
2854
  function chmodHooksSync(hookDir) {
@@ -3033,6 +3172,15 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
3033
3172
  )
3034
3173
  );
3035
3174
  }
3175
+ if (baseline.updateMode.policyBackedUp.length > 0) {
3176
+ log(
3177
+ assetRow(
3178
+ "skip",
3179
+ "edited policy files",
3180
+ `${baseline.updateMode.policyBackedUp.length} backed up as *.backup-<time>`
3181
+ )
3182
+ );
3183
+ }
3036
3184
  if (baseline.updateMode.staleHookRefs.length > 0) {
3037
3185
  log(
3038
3186
  assetRow(
@@ -3381,7 +3529,7 @@ function registerInstallCommand(cli2) {
3381
3529
 
3382
3530
  // src/commands/list.ts
3383
3531
  init_esm_shims();
3384
- import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
3532
+ import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
3385
3533
  import { join as join12, resolve as resolve3 } from "path";
3386
3534
  function listAction(options = {}, deps = {}) {
3387
3535
  const log = deps.log ?? console.log;
@@ -3446,7 +3594,7 @@ function formatTemplateRows(log, projectDir) {
3446
3594
  const rootMd = log.templates.rootClaudeMd;
3447
3595
  if (rootMd) {
3448
3596
  const path = join12(projectDir, rootMd.path);
3449
- const modified = existsSync13(path) && hashContent(readFileSync12(path, "utf8")) !== rootMd.sha256;
3597
+ const modified = existsSync14(path) && hashContent(readFileSync13(path, "utf8")) !== rootMd.sha256;
3450
3598
  rows.push(
3451
3599
  modified ? ` ${c.dim(rootMd.path)} ${c.yellow("(\uC218\uC815\uB428 \u2014 uninstall \uC2DC \uBCF4\uC874)")}` : ` ${c.dim(rootMd.path)}`
3452
3600
  );
@@ -3454,7 +3602,7 @@ function formatTemplateRows(log, projectDir) {
3454
3602
  return rows;
3455
3603
  }
3456
3604
  function formatRootFileRows(rootFiles, projectDir) {
3457
- const present = rootFiles.filter((f) => existsSync13(join12(projectDir, f.path)));
3605
+ const present = rootFiles.filter((f) => existsSync14(join12(projectDir, f.path)));
3458
3606
  const width = Math.max(0, ...present.map((f) => f.path.length));
3459
3607
  return present.map(
3460
3608
  (f) => ` ${c.dim(padDisplay(f.path, width))} ${c.dim(f.change === "created" ? "\uC0DD\uC131" : "\uBCD1\uD569")} ${c.dim(f.notes.join(" / "))}`
@@ -3471,7 +3619,7 @@ function registerListCommand(cli2) {
3471
3619
  // src/commands/uninstall.ts
3472
3620
  init_esm_shims();
3473
3621
  import { spawnSync as spawnSync2 } from "child_process";
3474
- import { existsSync as existsSync14, readFileSync as readFileSync13, rmSync } from "fs";
3622
+ import { existsSync as existsSync15, readFileSync as readFileSync14, rmSync } from "fs";
3475
3623
  import { join as join13, resolve as resolve4 } from "path";
3476
3624
 
3477
3625
  // src/uninstall-interactive.ts
@@ -4757,7 +4905,7 @@ function advisoryLines(plan, targetAssets, projectDir, templatesKept, rootFiles)
4757
4905
  return lines;
4758
4906
  }
4759
4907
  function rootFileAdvisoryLines(rootFiles, projectDir) {
4760
- const present = rootFiles.filter((f) => existsSync14(join13(projectDir, f.path)));
4908
+ const present = rootFiles.filter((f) => existsSync15(join13(projectDir, f.path)));
4761
4909
  if (present.length === 0) return [];
4762
4910
  return [
4763
4911
  "",
@@ -4830,9 +4978,9 @@ function buildProjectReverseStep(asset, spawn) {
4830
4978
  }
4831
4979
  function settingsHasHookCommand(projectDir, command) {
4832
4980
  const path = join13(projectDir, ".claude", "settings.json");
4833
- if (!existsSync14(path)) return false;
4981
+ if (!existsSync15(path)) return false;
4834
4982
  try {
4835
- const parsed = JSON.parse(readFileSync13(path, "utf8"));
4983
+ const parsed = JSON.parse(readFileSync14(path, "utf8"));
4836
4984
  return Object.values(parsed.hooks ?? {}).some(
4837
4985
  (matchers) => matchers.some((m) => m.hooks?.some((h2) => h2.command === command))
4838
4986
  );
@@ -4857,7 +5005,7 @@ function manualAdvisoryLines(targetAssets, projectDir) {
4857
5005
  `.claude/settings.json \u2014 hooks.PreToolUse \uC5D0\uC11C \uB2E4\uC74C command \uD56D\uBAA9 \uC0AD\uC81C:
4858
5006
  ${KARPATHY_HOOK_COMMAND}`
4859
5007
  );
4860
- if (existsSync14(join13(projectDir, KARPATHY_HOOK_RELPATH)))
5008
+ if (existsSync15(join13(projectDir, KARPATHY_HOOK_RELPATH)))
4861
5009
  items.push(`\`${KARPATHY_HOOK_RELPATH}\` \u2014 \uC0AD\uC81C`);
4862
5010
  if (items.length === 0) return [];
4863
5011
  return [
@@ -4901,8 +5049,8 @@ function rootClaudeMdModified(log, projectDir) {
4901
5049
  const rootMd = log.templates.rootClaudeMd;
4902
5050
  if (!rootMd) return false;
4903
5051
  const path = join13(projectDir, rootMd.path);
4904
- if (!existsSync14(path)) return false;
4905
- return hashContent(readFileSync13(path, "utf8")) !== rootMd.sha256;
5052
+ if (!existsSync15(path)) return false;
5053
+ return hashContent(readFileSync14(path, "utf8")) !== rootMd.sha256;
4906
5054
  }
4907
5055
  function formatTemplateList(log) {
4908
5056
  const items = [log.templates.claudeDir];
@@ -4914,7 +5062,7 @@ function defaultSpawn2(cmd, args) {
4914
5062
  return spawnSync2(cmd, [...args], { encoding: "utf8", stdio: "pipe", timeout: 12e4 });
4915
5063
  }
4916
5064
  function defaultRm(path) {
4917
- if (existsSync14(path)) {
5065
+ if (existsSync15(path)) {
4918
5066
  rmSync(path, { recursive: true, force: true });
4919
5067
  }
4920
5068
  }
@@ -4962,7 +5110,7 @@ import { resolve as resolve5 } from "path";
4962
5110
 
4963
5111
  // src/state.ts
4964
5112
  init_esm_shims();
4965
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
5113
+ import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
4966
5114
  import { join as join14 } from "path";
4967
5115
  var META_FILE = ".claude/.installed-tracks";
4968
5116
  var LEGACY_SIGNATURES = [
@@ -4974,12 +5122,12 @@ var LEGACY_SIGNATURES = [
4974
5122
  ];
4975
5123
  function detectInstallState(projectDir) {
4976
5124
  const claudeDir = join14(projectDir, ".claude");
4977
- const hasClaudeDir = existsSync15(claudeDir);
5125
+ const hasClaudeDir = existsSync16(claudeDir);
4978
5126
  if (!hasClaudeDir) {
4979
5127
  return { state: "new", tracks: [], source: "none", hasClaudeDir: false };
4980
5128
  }
4981
5129
  const metaPath = join14(projectDir, META_FILE);
4982
- if (existsSync15(metaPath)) {
5130
+ if (existsSync16(metaPath)) {
4983
5131
  const tracks2 = readMetafile(metaPath);
4984
5132
  return { state: "existing", tracks: tracks2, source: "metafile", hasClaudeDir: true };
4985
5133
  }
@@ -4987,7 +5135,7 @@ function detectInstallState(projectDir) {
4987
5135
  return { state: "existing", tracks, source: "legacy", hasClaudeDir: true };
4988
5136
  }
4989
5137
  function readMetafile(path) {
4990
- const raw = readFileSync14(path, "utf8");
5138
+ const raw = readFileSync15(path, "utf8");
4991
5139
  const seen = /* @__PURE__ */ new Set();
4992
5140
  for (const line of raw.split(/\s+/)) {
4993
5141
  const trimmed = line.trim();
@@ -4999,12 +5147,12 @@ function readMetafile(path) {
4999
5147
  }
5000
5148
  function inferFromLegacySignatures(projectDir) {
5001
5149
  const rulesDir = join14(projectDir, ".claude/rules");
5002
- if (!existsSync15(rulesDir)) {
5150
+ if (!existsSync16(rulesDir)) {
5003
5151
  return [];
5004
5152
  }
5005
5153
  const found = /* @__PURE__ */ new Set();
5006
5154
  for (const sig of LEGACY_SIGNATURES) {
5007
- if (existsSync15(join14(rulesDir, sig.rule))) {
5155
+ if (existsSync16(join14(rulesDir, sig.rule))) {
5008
5156
  found.add(sig.track);
5009
5157
  }
5010
5158
  }