@uzysjung/agent-harness 26.168.0 → 26.169.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
@@ -17,10 +17,12 @@ import {
17
17
  INTERNAL_BUNDLED_SKILL_IDS,
18
18
  LEGACY_INSTALL_LOG_DIR,
19
19
  POLICY_DIRS,
20
+ PROJECT_SKELETON_DIRS,
20
21
  REMOVED_CATALOG_ASSETS,
21
22
  RENAMED_SKILL_IDS,
22
23
  RETIRED_AGENTS,
23
24
  RETIRED_AGENT_IDS,
25
+ RETIRED_HOOK_WIRING_REFS,
24
26
  RETIRED_PATHS,
25
27
  RETIRED_SKILL_IDS,
26
28
  SHARED_FILES,
@@ -35,10 +37,11 @@ import {
35
37
  assetCliSupport,
36
38
  assetReachesCli,
37
39
  assetTrustTier,
38
- backupDir,
40
+ backupContent,
39
41
  backupFile,
40
42
  backupIfLossyUtf8,
41
43
  backupOriginal,
44
+ buildAssetEntries,
42
45
  buildAssetSpec,
43
46
  buildInstallLog,
44
47
  buildManifest,
@@ -47,12 +50,10 @@ import {
47
50
  collectPolicyHashes,
48
51
  collectSkillHashes,
49
52
  continuousNoteEnd,
50
- copyBackupDir,
51
53
  copyDir,
52
54
  copyFile,
53
55
  corruptedInstallLogMessage,
54
56
  createOutsideGuard,
55
- dirTreesIdentical,
56
57
  dropContinuousSkillsNote,
57
58
  dropHarnessQuoteBlocks,
58
59
  ensureDir,
@@ -103,7 +104,7 @@ import {
103
104
  targetsInclude,
104
105
  upsertHarnessImport,
105
106
  writeInstallLog
106
- } from "./chunk-V4UY222W.js";
107
+ } from "./chunk-SU2RUN2W.js";
107
108
 
108
109
  // node_modules/sisteransi/src/index.js
109
110
  var require_src = __commonJS({
@@ -787,8 +788,7 @@ var cac = (name = "") => new CAC(name);
787
788
 
788
789
  // src/commands/install.ts
789
790
  init_esm_shims();
790
- import { rmSync as rmSync4 } from "fs";
791
- import { join as join32, resolve as resolve2 } from "path";
791
+ import { resolve as resolve2 } from "path";
792
792
  import { fileURLToPath } from "url";
793
793
 
794
794
  // src/adapters/index.ts
@@ -827,45 +827,59 @@ function keepHookRef(hook, claudeDir, removed) {
827
827
  if (!exists && !removed.includes(relPath)) removed.push(relPath);
828
828
  return exists;
829
829
  }
830
- function scanStaleHookRefs(settingsPath, claudeDir, spare) {
830
+ function findStaleHookRefs(settingsPath, claudeDir, spare) {
831
831
  let settings;
832
832
  try {
833
833
  settings = JSON.parse(readFileSync(settingsPath, "utf8"));
834
834
  } catch {
835
- return null;
835
+ return [];
836
+ }
837
+ const stale = [];
838
+ for (const eventEntries of Object.values(settings.hooks ?? {})) {
839
+ if (!Array.isArray(eventEntries)) continue;
840
+ for (const entry of eventEntries) {
841
+ if (!Array.isArray(entry?.hooks)) continue;
842
+ for (const hook of entry.hooks) {
843
+ if (spare?.(projectAnchoredRef(hook?.command ?? "", claudeDir) ?? "") === true) continue;
844
+ keepHookRef(hook, claudeDir, stale);
845
+ }
846
+ }
847
+ }
848
+ return stale;
849
+ }
850
+ function removeRetiredHookRefs(settingsPath, claudeDir, refs, opts = {}) {
851
+ let settings;
852
+ try {
853
+ settings = JSON.parse(readFileSync(settingsPath, "utf8"));
854
+ } catch {
855
+ return [];
836
856
  }
837
- const hookEvents = settings.hooks ?? {};
838
857
  const removed = [];
839
- const cleanedHooks = {};
840
- for (const [eventName, eventEntries] of Object.entries(hookEvents)) {
841
- if (!Array.isArray(eventEntries)) {
842
- cleanedHooks[eventName] = eventEntries;
858
+ const isRetiredDead = (hook) => {
859
+ const rel = projectAnchoredRef(hook?.command ?? "", claudeDir);
860
+ if (rel === void 0 || !refs.includes(rel)) return false;
861
+ if (opts.evenIfScriptExists !== true && existsSync(join(claudeDir, rel))) return false;
862
+ if (!removed.includes(rel)) removed.push(rel);
863
+ return true;
864
+ };
865
+ const hooks = {};
866
+ for (const [event, entries] of Object.entries(settings.hooks ?? {})) {
867
+ if (!Array.isArray(entries)) {
868
+ hooks[event] = entries;
843
869
  continue;
844
870
  }
845
- cleanedHooks[eventName] = eventEntries.filter((entry) => Array.isArray(entry?.hooks)).map((entry) => ({
846
- ...entry,
847
- hooks: entry.hooks.filter(
848
- (hook) => (
849
- // `spare` 가 고른 참조(기록된 하네스 스크립트 — update 가 되살리고 배선은 writer 가 소유한다)는 판정하지 않는다
850
- spare?.(projectAnchoredRef(hook?.command ?? "", claudeDir) ?? "") === true || keepHookRef(hook, claudeDir, removed)
851
- )
852
- )
853
- })).filter((entry) => entry.hooks.length > 0);
871
+ hooks[event] = entries.flatMap((entry) => {
872
+ if (!Array.isArray(entry?.hooks)) return [entry];
873
+ const kept = entry.hooks.filter((hook) => !isRetiredDead(hook));
874
+ if (kept.length === entry.hooks.length) return [entry];
875
+ return kept.length > 0 ? [{ ...entry, hooks: kept }] : [];
876
+ });
854
877
  }
855
- return { settings, cleanedHooks, removed };
856
- }
857
- function findStaleHookRefs(settingsPath, claudeDir) {
858
- return scanStaleHookRefs(settingsPath, claudeDir)?.removed ?? [];
859
- }
860
- function cleanStaleHookRefs(settingsPath, claudeDir, spare) {
861
- const scan = scanStaleHookRefs(settingsPath, claudeDir, spare);
862
- if (scan === null) return [];
863
- if (scan.removed.length > 0) {
864
- const next = { ...scan.settings, hooks: scan.cleanedHooks };
865
- writeFileSync(settingsPath, `${JSON.stringify(next, null, 2)}
878
+ if (removed.length > 0 && opts.write !== false) {
879
+ writeFileSync(settingsPath, `${JSON.stringify({ ...settings, hooks }, null, 2)}
866
880
  `);
867
881
  }
868
- return scan.removed;
882
+ return removed;
869
883
  }
870
884
 
871
885
  // src/adapters/contract.ts
@@ -1424,6 +1438,125 @@ init_esm_shims();
1424
1438
 
1425
1439
  // src/agents-md-merge.ts
1426
1440
  init_esm_shims();
1441
+
1442
+ // src/retired-anchor-sections.ts
1443
+ init_esm_shims();
1444
+ import { createHash } from "crypto";
1445
+ function retiredAnchorSectionKey(lines2) {
1446
+ const out = lines2.map((l) => l.replace(/\r$/, "").trimEnd());
1447
+ while (out.length > 1 && out.at(-1) === "") out.pop();
1448
+ return createHash("sha256").update(out.join("\n")).digest("hex");
1449
+ }
1450
+ var RETIRED_ANCHOR_SECTION_KEYS = /* @__PURE__ */ new Set([
1451
+ "522bac15bc17949697e291b837c0d9624f5e0c7666400a3baf94cdef7cfab8b5",
1452
+ // ## Rule 1 — Think Before Coding
1453
+ "ea10b7cd9b84a71949a071fae15256f490b3fe407159d7bddcf3f882310fa835",
1454
+ // ## Rule 2 — Simplicity First
1455
+ "c486465d9554f2de1711cd056437f6a95a5d3243264b82d29a6957f24b5a6de3",
1456
+ // ## Rule 3 — Surgical Changes
1457
+ "3d080574cc7ae81eda75c4aa019445b38b6f69c5f9eeeda9097e10b6041e7e94",
1458
+ // ## Rule 4 — Goal-Driven Execution
1459
+ "11e43547f6b1613a5dabb3edaf5131d696a3fd375b3c22558f03555a0f5cf2ef",
1460
+ // ## Rule 5 — Use the model only for judgment calls
1461
+ "60d91718de6ae16ec86b50e7c9a939a6ca5fc0721422b592700cc9972f55aca2",
1462
+ // ## Rule 6 — Token budgets are not advisory
1463
+ "9d482c7307cbcb02acc6fb35cd57b24baa9fce04528e2bd21e1e877603f8b85b",
1464
+ // ## Rule 7 — Surface conflicts, don't average them
1465
+ "abca09f13833ce18bd0ffc0793d59780c35447ca79c2a512afcbc9d7a7116ed8",
1466
+ // ## Rule 8 — Read before you write
1467
+ "5b4eae6c3dc2d92cfb1772889650882a4a2115f08a36602da9a1d96697059b91",
1468
+ // ## Rule 9 — Tests verify intent, not just behavior
1469
+ "632670b8897dc373d4a12f611c2436d9a8e86d8cdab917eecabc4bf2a56f8030",
1470
+ // ## Rule 10 — Checkpoint after every significant step
1471
+ "252e99f25b05a47e5ce884ac9f1e1d34156116b9a76331e2510b1354a6c98cd4",
1472
+ // ## Rule 11 — Match the codebase's conventions, even if you disagree
1473
+ "c28a12b589b6576379793c51fd01dbfaf91d391ac773f84ae0477ddb60a1e3e8",
1474
+ // ## Rule 12 — Fail loud
1475
+ "72feaf96f998989351f8083edb891c7b0c3bccca65dbea2732fb2864b8f98bd4",
1476
+ // ## 의사결정 및 컨펌 요청 시
1477
+ "95c932ecb4f4fdcdd8d483f9d02166b906f6eb045ef33ee474a16913d0c3e6cd",
1478
+ // ## Phase/작업 완료 시 Self-Audit 실행:
1479
+ "c7f9fa755bd66d80c2df496c6c759e8accd736cd8fef64460feb7ae9cd4c8006",
1480
+ // ## Context Management
1481
+ "6ec0b415a8f86c9940bccd3e6db895ea4a110185e76fef223a348f9553fd897c",
1482
+ // ## Rule 12 — Fail loud
1483
+ "a78f0ffa6b51c9b45c964ecb92db951534336f7e558a3d39d11f015ee6c3466c",
1484
+ // ## Anti-Patterns (Forbidden)
1485
+ "50d00a511e87571d7ff85f6584b87b9f5c2eef06122d8ac5fdccf3f047643f12",
1486
+ // ## When Requesting Decisions or Confirmation
1487
+ "574519e07f48bcc04f0e3d505c0f9c157ddd426b5eda2baa99656cd4712dad2b",
1488
+ // ## Run Self-Audit on Phase/Task Completion
1489
+ "df73823c02e6eee000388444964acfec233ffeec0e20fdc5d31eb018d2ca93c1",
1490
+ // ## Context Management
1491
+ "8f49f0a592575a65eadd3e33950c96616223aa8bb116707103affeef71db28c6",
1492
+ // ## Delegate the Building, Keep the Deciding
1493
+ "6115eccc3b2702f436a9db7d6af9803fa7b1fbf7b9ff1110ea658fadd67ec5ea",
1494
+ // ## 1. Think before coding
1495
+ "15b120395ec8e757ecbe3e65775371a9120953617fb37eb062e0c176b52f045a",
1496
+ // ## 2. Prefer the simplest sufficient solution
1497
+ "cc466b4781444ca54aedebd20ba3aa531c3aa84cc438953df84b133bd32a0e1e",
1498
+ // ## 3. Make surgical changes and preserve existing work
1499
+ "f6d1f6f02cb3c19e61becf329f20335c54e684bccb06b33e4b7f66eb49f9c6b6",
1500
+ // ## 4. Define success before editing
1501
+ "372bf30b282b861c6a89e355c94ffd239bb0202e2ef928f8af99d640579710f0",
1502
+ // ## 5. Report evidence, not confidence
1503
+ "66ca0e3aae98d24c65afbc418645df29d8c570be81918c42192eb1beecd21f23",
1504
+ // ## 6. Do not cross high-impact boundaries alone
1505
+ "9c6c27eb57a9790273fa3fdf0df3628d927b025f870f09933692d1b522d73a25",
1506
+ // ## Decisions and explanations
1507
+ "922d100c2dc6ef8a53989a920c3554afa7efd148255bc144412403c0289439e6",
1508
+ // ## Decisions and explanations
1509
+ "330a67775a28557431aebf36a44018fc5194a4b929bf2cf95184b1f0828a6360",
1510
+ // ## 1. Understand First
1511
+ "3b2f0147dc7bd0f01fd12485790199ef4ee96a6b723e244ffbf3dc96933a1ab8",
1512
+ // ## 2. Define Success and Keep It Simple
1513
+ "8802fb25c05879c98dbf8407001d239c410d25b4fa2521b11d9d7cc108f73b29",
1514
+ // ## 3. Preserve Sound Boundaries
1515
+ "1d0d9bf05d8870dd0dc62e388ceffc98923f1dd2074ef8038f80a50c9b5b9a81",
1516
+ // ## 4. Make Surgical Changes
1517
+ "05e08e2318061141464ae714d6770d35527576affa787740d9c6d9345d3462dd",
1518
+ // ## 5. Verify and Review
1519
+ "7b39f1f92a72517d6ae52599becfef29f675e48185a3483dc993d7accd4aee7c",
1520
+ // ## 6. Protect High-Impact Boundaries
1521
+ "d5d93c55de33e4cdcb38f76c6d6d70a6e0beac7a176c8aa1daa09905ccc5fe39",
1522
+ // ## 7. Report Evidence
1523
+ "cefc8bb6194d4f1b6302345e96ffb3a28b8b992032fd40f9f5b4f18997657e7a",
1524
+ // ## Presenting a decision
1525
+ "7385ff5df34148278a8f4b164c11ba5f9355626abcc13a822dd1ebb206321c2f",
1526
+ // ## Skills that apply continuously
1527
+ "ad9fd1ff225c57c9801a7d7472bf4ddf9c952c602f4a93eae86fbcb3bcf468bc",
1528
+ // ## 7. Report Evidence
1529
+ "868fe823fc40011d08816c9f1105ef6df7ca7f2479779303ce9583bba1ce6e0d",
1530
+ // ## Skills that apply continuously
1531
+ "3f8259ca7c92d79cca1a36bba38f36d4ef5a96fd6cafbd0edb5a79492082f46f",
1532
+ // ## Presenting a decision
1533
+ "ecfdcd50e97723af77cfe6889bbad784cdeb86dd0c52af3ff98ddf6adf80f445",
1534
+ // ## 1. Resolve what matters, then act
1535
+ "9321cb1312a080bcf560940b8f86d3d59b39c667add4b8f3ad785ddae9bc8b20",
1536
+ // ## 2. Choose the simplest sufficient solution
1537
+ "a4c345996a8578cc24b62fde27084e5848a61e4a40d91b0c72a95da46203b43c",
1538
+ // ## 3. Keep changes focused and preserve existing work
1539
+ "1adbf1bf88bb8288744e1c1d73518ffdec12a5b668d5a225e2bedd006844466c",
1540
+ // ## 4. Define success and verify proportionally
1541
+ "7636e57a877d54039f0c2829fde84202c65378bd36b8c8abb7274a29eaa5f8ac",
1542
+ // ## 5. Report evidence and stop unproductive loops
1543
+ "ddf2be9e04c04bf53ad9b5476e45639b33a60bf49bd3a83835796632052b17b4",
1544
+ // ## 6. Keep authority explicit
1545
+ "16b0edc0f4886a9f9ec94e6fb97206c385286b2c9d5563b2324b7a2f4a434435",
1546
+ // ## 1. Understand what success means
1547
+ "b10ab1eafe25bb7ba3b20b907b1d7eedde03e52fbd49905f24e192e30b4e1b9a",
1548
+ // ## 2. Choose and adapt the approach
1549
+ "69bcfb8b37ac184b1a30945a0dea59108ef1242c2f40fb778be118cbd524aea7",
1550
+ // ## 3. Keep changes focused and coherent
1551
+ "f22a5a13c5ed8460c9370212d91354d0fea2fefc7f420a1eb3088504ae8da6c0",
1552
+ // ## 4. Verify outcomes in proportion to consequences
1553
+ "7a335cde69e8055ee5e137a9a5c09fd7f7c2e551ab099a2cda87f0c3dcd95a19",
1554
+ // ## 5. Let evidence determine readiness
1555
+ "79797568bb71bd0f7b14393f72e61721a4bfe99ecfb6126ffa38a6c03c53d765"
1556
+ // ## 6. Act within clear authority
1557
+ ]);
1558
+
1559
+ // src/agents-md-merge.ts
1427
1560
  var marker = (name) => ({
1428
1561
  start: `<!-- uzys-harness:${name}:start -->`,
1429
1562
  end: `<!-- uzys-harness:${name}:end -->`
@@ -1562,8 +1695,11 @@ function mergeAgentsMd(params) {
1562
1695
  }
1563
1696
  const rulesNow = sectionBody(existing, names, RULES_SECTION);
1564
1697
  const rulesNew = sectionBody(rendered, names, RULES_SECTION);
1698
+ let replacedRulesHeadings = null;
1565
1699
  if (rulesNow !== null && rulesNew !== null && blockRange(rulesNow, ANCHOR_BLOCK) !== null) {
1566
1700
  bodies.set(RULES_SECTION, refreshBlock(rulesNow, rulesNew, ANCHOR_BLOCK));
1701
+ } else if (rulesNow !== null && rulesNew !== null) {
1702
+ replacedRulesHeadings = headingLines(rulesNew);
1567
1703
  }
1568
1704
  const merged = bodies.size === 0 ? rendered : assemble(rendered, names, bodies);
1569
1705
  const mergedText = merged.replace(/\r/g, "");
@@ -1571,8 +1707,8 @@ function mergeAgentsMd(params) {
1571
1707
  existing,
1572
1708
  names,
1573
1709
  rendered,
1574
- template,
1575
- params.harnessRuleHeadings ?? /* @__PURE__ */ new Set()
1710
+ params.harnessRuleHeadings ?? /* @__PURE__ */ new Set(),
1711
+ replacedRulesHeadings
1576
1712
  ).filter((e2) => !mergedText.includes(e2.join("\n").replace(/\r/g, "")));
1577
1713
  if (extras.length === 0) return merged;
1578
1714
  return `${merged.replace(/\n+$/, "\n")}${extras.map((e2) => e2.join("\n")).join("\n")}`.replace(
@@ -1580,24 +1716,48 @@ function mergeAgentsMd(params) {
1580
1716
  "\n"
1581
1717
  );
1582
1718
  }
1583
- function extraTopLevelSections(existing, names, rendered, template, harnessRuleHeadings) {
1719
+ function headingOwner(bare, ctx) {
1720
+ const name = /^## (.+?)\s*$/.exec(bare)?.[1];
1721
+ if (name !== void 0 && ctx.names.has(name)) return "harness";
1722
+ if (ctx.region === CONTEXT_SECTION || ctx.region === RULES_SECTION) return "body";
1723
+ if (ctx.region === HARNESS_RULES_SECTION && ctx.harnessHeadings.has(bare.trim())) {
1724
+ return "harness";
1725
+ }
1726
+ return "installer";
1727
+ }
1728
+ function headingLines(body) {
1729
+ return new Set(body.filter((l) => /^## /.test(l)).map((l) => l.replace(/\r$/, "").trim()));
1730
+ }
1731
+ function renderedRuleHeadings(rendered, names) {
1732
+ return headingLines(sectionBody(rendered, names, HARNESS_RULES_SECTION) ?? []);
1733
+ }
1734
+ function extraTopLevelSections(existing, names, rendered, harnessRuleHeadings, replacedRulesHeadings) {
1735
+ const harnessHeadings = /* @__PURE__ */ new Set([
1736
+ ...harnessRuleHeadings,
1737
+ ...renderedRuleHeadings(rendered, names)
1738
+ ]);
1584
1739
  const out = [];
1740
+ const replacedSections = /* @__PURE__ */ new Set();
1585
1741
  let current = null;
1586
- let inHarnessRules = false;
1742
+ let region = null;
1587
1743
  for (const line of existing.split("\n")) {
1588
1744
  const bare = line.replace(/\r$/, "");
1589
1745
  const name = /^## (.+?)\s*$/.exec(bare)?.[1];
1590
1746
  if (name !== void 0) {
1591
1747
  if (current !== null) out.push(current);
1592
- if (names.has(name)) inHarnessRules = name === HARNESS_RULES_SECTION;
1593
- const harnessAuthored = names.has(name) || inHarnessRules && harnessRuleHeadings.has(bare.trim()) || rendered.includes(bare) || template.includes(bare) || line.trim() === CONTINUOUS_SKILLS_HEADING;
1594
- current = harnessAuthored ? null : [line];
1748
+ if (names.has(name)) region = name;
1749
+ const owner = headingOwner(bare, { region, names, harnessHeadings });
1750
+ const replacedBody = owner === "body" && region === RULES_SECTION && replacedRulesHeadings !== null && !replacedRulesHeadings.has(bare.trim());
1751
+ current = owner === "installer" || replacedBody ? [line] : null;
1752
+ if (replacedBody && current !== null) replacedSections.add(current);
1595
1753
  continue;
1596
1754
  }
1597
1755
  if (current !== null) current.push(line);
1598
1756
  }
1599
1757
  if (current !== null) out.push(current);
1600
- return out.map((lines2) => {
1758
+ return out.filter(
1759
+ (lines2) => !replacedSections.has(lines2) || !RETIRED_ANCHOR_SECTION_KEYS.has(retiredAnchorSectionKey(lines2))
1760
+ ).map((lines2) => {
1601
1761
  const trimmed = [...lines2];
1602
1762
  while (trimmed.length > 1 && (trimmed.at(-1) ?? "").trim() === "") trimmed.pop();
1603
1763
  return trimmed;
@@ -1630,14 +1790,9 @@ function installerSectionsInHarnessArea(existing, names, harnessRuleHeadings) {
1630
1790
  const name = /^## (.+?)\s*$/.exec(bare)?.[1];
1631
1791
  if (name !== void 0) {
1632
1792
  if (current !== null) out.push(current);
1633
- current = null;
1634
- if (names.has(name)) {
1635
- region = name;
1636
- continue;
1637
- }
1638
- if (region === CONTEXT_SECTION || region === RULES_SECTION) continue;
1639
- const harnessRule = region === HARNESS_RULES_SECTION && harnessRuleHeadings.has(bare.trim());
1640
- if (!harnessRule) current = [bare];
1793
+ if (names.has(name)) region = name;
1794
+ const owner = headingOwner(bare, { region, names, harnessHeadings: harnessRuleHeadings });
1795
+ current = owner === "installer" ? [bare] : null;
1641
1796
  continue;
1642
1797
  }
1643
1798
  if (current !== null) current.push(bare);
@@ -2993,8 +3148,8 @@ function padDisplay(s, width) {
2993
3148
 
2994
3149
  // src/install-writes.ts
2995
3150
  init_esm_shims();
2996
- import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2997
- import { dirname, join as join6, relative, sep } from "path";
3151
+ import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync3 } from "fs";
3152
+ import { dirname, join as join7, relative, sep } from "path";
2998
3153
 
2999
3154
  // src/env-files.ts
3000
3155
  init_esm_shims();
@@ -3003,8 +3158,9 @@ import { join as join4 } from "path";
3003
3158
  var ENV_EXAMPLE_BODY = `# .env.example \u2014 csr-supabase Track
3004
3159
  # Copy to .env (gitignored) and fill in values: cp .env.example .env
3005
3160
 
3006
- # ===== Supabase Management API (MCP server\uC6A9) =====
3007
- # Personal Access Token \u2014 @supabase/mcp-server\uAC00 \uD504\uB85C\uC81D\uD2B8 \uC0DD\uC131/\uB9C8\uC774\uADF8\uB808\uC774\uC158/Edge Functions \uBC30\uD3EC\uC5D0 \uC0AC\uC6A9
3161
+ # ===== Supabase Management API =====
3162
+ # MCP \uC11C\uBC84(https://mcp.supabase.com/mcp)\uB294 \uCCAB \uC0AC\uC6A9 \uB54C \uBE0C\uB77C\uC6B0\uC800 OAuth \uB85C\uADF8\uC778 \u2014 \uC774 \uD1A0\uD070\uC774 \uD544\uC694 \uC5C6\uB2E4.
3163
+ # Personal Access Token \u2014 \uB85C\uCEEC stdio \uC11C\uBC84(@supabase/mcp-server-supabase)\uB098 CI \uCC98\uB7FC \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uC774 \uC548 \uB418\uB294 \uACF3\uC5D0\uC11C\uB9CC
3008
3164
  # \uBC1C\uAE09: https://supabase.com/dashboard/account/tokens
3009
3165
  SUPABASE_ACCESS_TOKEN=
3010
3166
 
@@ -3169,6 +3325,28 @@ function advisory(input) {
3169
3325
  if (input.run === "update") return j("leave", "", "keep");
3170
3326
  return j("create", LINES.scaffoldWrote, "advisory");
3171
3327
  }
3328
+ function judgeDisplaced(input) {
3329
+ if (input.backup === null) return { verdict: "leave", line: "", record: "forget" };
3330
+ if (!input.backupExists) {
3331
+ return {
3332
+ verdict: "advise",
3333
+ line: "could not put back your earlier file \u2014 its backup is no longer there",
3334
+ record: "forget"
3335
+ };
3336
+ }
3337
+ if (!input.slotEmpty) {
3338
+ return {
3339
+ verdict: "advise",
3340
+ line: `not put back \u2014 the spot is taken; your earlier file is at ${input.backup}`,
3341
+ record: "keep"
3342
+ };
3343
+ }
3344
+ return {
3345
+ verdict: "restore",
3346
+ line: `put back your earlier file (from ${input.backup})`,
3347
+ record: "forget"
3348
+ };
3349
+ }
3172
3350
 
3173
3351
  // src/mcp-merge.ts
3174
3352
  init_esm_shims();
@@ -3188,6 +3366,12 @@ var PRIOR_MCP_SERVERS = [
3188
3366
  value: { type: "stdio", command: "npx", args: ["-y", "@railway/mcp-server"] },
3189
3367
  updated: "now Railway's remote server \u2014 sign in on first use, see NEXT"
3190
3368
  },
3369
+ {
3370
+ // #766 — `@supabase/mcp-server` 는 npm 에 없는 이름이라(404) 서버가 뜬 적이 없다
3371
+ name: "supabase",
3372
+ value: { type: "stdio", command: "npx", args: ["-y", "@supabase/mcp-server"] },
3373
+ updated: "now Supabase's remote server \u2014 sign in on first use, see NEXT"
3374
+ },
3191
3375
  {
3192
3376
  name: "chrome-devtools",
3193
3377
  value: { type: "stdio", command: "npx", args: ["-y", "chrome-devtools-mcp@latest"] },
@@ -3366,6 +3550,8 @@ function recordedMcpAssetIds(log) {
3366
3550
 
3367
3551
  // src/recorded.ts
3368
3552
  init_esm_shims();
3553
+ import { existsSync as existsSync5 } from "fs";
3554
+ import { join as join6 } from "path";
3369
3555
 
3370
3556
  // src/cli-ownership.ts
3371
3557
  init_esm_shims();
@@ -3379,19 +3565,19 @@ var SHARED_BY_ALL = (self) => [
3379
3565
  ];
3380
3566
  var CLI_OWNERSHIP = {
3381
3567
  claude: [
3382
- { path: ".claude/", kind: "dir", sharedWith: [] },
3568
+ { path: ".claude/", kind: "recorded", sharedWith: [] },
3383
3569
  { path: HARNESS_ANCHOR_FILE, kind: "anchor", sharedWith: [] },
3384
3570
  { path: "CLAUDE.md", kind: "import-block", sharedWith: [] },
3385
3571
  ...SHARED_BY_ALL("claude")
3386
3572
  ],
3387
3573
  codex: [
3388
- { path: ".codex/", kind: "dir", sharedWith: [] },
3574
+ { path: ".codex/", kind: "recorded", sharedWith: [] },
3389
3575
  { path: "AGENTS.md", kind: "recorded", sharedWith: ["opencode", "antigravity"] },
3390
3576
  { path: ".agents/skills/", kind: "recorded", sharedWith: ["opencode", "antigravity"] },
3391
3577
  ...SHARED_BY_ALL("codex")
3392
3578
  ],
3393
3579
  opencode: [
3394
- { path: ".opencode/", kind: "dir", sharedWith: [] },
3580
+ { path: ".opencode/", kind: "recorded", sharedWith: [] },
3395
3581
  { path: "opencode.json", kind: "recorded", sharedWith: [] },
3396
3582
  { path: "AGENTS.md", kind: "recorded", sharedWith: ["codex", "antigravity"] },
3397
3583
  { path: ".agents/skills/", kind: "recorded", sharedWith: ["codex", "antigravity"] },
@@ -3440,14 +3626,36 @@ function ownersOf(path) {
3440
3626
  );
3441
3627
  }
3442
3628
  function isRecordedTrackTarget(log, path) {
3629
+ return recordedTrackTargets(log)(path);
3630
+ }
3631
+ function recordedTrackTargets(log) {
3443
3632
  const spec = {
3444
3633
  tracks: log.spec.tracks.filter(isTrack),
3445
3634
  selectedInternalSkills: INTERNAL_BUNDLED_SKILL_IDS
3446
3635
  };
3447
- return buildManifest(spec).some(
3448
- (e2) => e2.applies(spec) && (e2.type === "dir" ? path.startsWith(`${e2.target}/`) : e2.target === path)
3636
+ const entries = buildManifest(spec).filter((e2) => e2.applies(spec));
3637
+ const templates = join6(resolveBundleRoot(), "templates");
3638
+ return (path) => entries.some(
3639
+ (e2) => e2.type === "dir" ? path.startsWith(`${e2.target}/`) && existsSync5(join6(templates, e2.source, path.slice(e2.target.length + 1))) : e2.target === path
3449
3640
  );
3450
3641
  }
3642
+ function recordedTrackSpots(log) {
3643
+ const spec = {
3644
+ tracks: log.spec.tracks.filter(isTrack),
3645
+ selectedInternalSkills: INTERNAL_BUNDLED_SKILL_IDS
3646
+ };
3647
+ const templates = join6(resolveBundleRoot(), "templates");
3648
+ const out = [];
3649
+ for (const e2 of buildManifest(spec)) {
3650
+ if (!e2.applies(spec) || !e2.target.startsWith(CLAUDE_DIR)) continue;
3651
+ if (e2.type === "file") out.push(e2.target);
3652
+ else
3653
+ for (const rel of listFilesRecursive(join6(templates, e2.source)))
3654
+ out.push(`${e2.target}/${rel}`);
3655
+ }
3656
+ for (const p2 of RETIRED_PATHS) out.push(`${CLAUDE_DIR}${p2}`);
3657
+ return out;
3658
+ }
3451
3659
  function isRetiredPath(path) {
3452
3660
  return path.startsWith(CLAUDE_DIR) && RETIRED_PATHS.includes(path.slice(CLAUDE_DIR.length));
3453
3661
  }
@@ -3480,9 +3688,30 @@ function recorded(log, path, opts = {}) {
3480
3688
  const owners = ownersOf(path);
3481
3689
  const ownedByLog = owners.some((c3) => clis.includes(c3));
3482
3690
  const claudePath = path.startsWith(CLAUDE_DIR);
3483
- if (ownedByLog && isTarget(path) && (!claudePath || legacyOwned())) return { state: "no-sha" };
3691
+ if (ownedByLog && (claudePath ? legacyOwned() : isTarget(path))) {
3692
+ if (scanFieldOf(log, path) === "present") return { state: "none" };
3693
+ return { state: "no-sha" };
3694
+ }
3484
3695
  return { state: "none" };
3485
3696
  }
3697
+ function scanFieldOf(log, path) {
3698
+ const present2 = (yes) => yes ? "present" : "absent";
3699
+ if (path.startsWith(SKILLS_DIR)) return present2(log.skillFiles !== void 0);
3700
+ if (isExternalOnly(path))
3701
+ return present2((log.externalFiles ?? []).some((f) => isExternalOnly(f.path)));
3702
+ const policy = POLICY_DIRS.find(
3703
+ ({ dir, ext }) => path.startsWith(`${CLAUDE_DIR}${dir}/`) && path.endsWith(ext)
3704
+ );
3705
+ if (policy === void 0) return "outside";
3706
+ return present2((log.policyFiles ?? []).some((f) => f.path.startsWith(`${policy.dir}/`)));
3707
+ }
3708
+ function isExternalOnly(path) {
3709
+ const owners = ownersOf(path);
3710
+ return owners.length > 0 && !owners.includes("claude");
3711
+ }
3712
+ function recordedOwner(log) {
3713
+ return (path) => recorded(log, path, { isTarget: () => true }).state !== "none";
3714
+ }
3486
3715
  function kindOf(log, path) {
3487
3716
  if (adapterFor(path) !== null) return "shared";
3488
3717
  if (isScaffoldPath(path)) return "advisory";
@@ -3520,10 +3749,10 @@ function createInstallWriter(args) {
3520
3749
  else external.set(path, sha256);
3521
3750
  }
3522
3751
  function harness(path, from, opts = {}) {
3523
- const abs = join6(projectDir, path);
3752
+ const abs = join7(projectDir, path);
3524
3753
  if (outside.skip(abs)) return null;
3525
3754
  const next = "source" in from ? readFileSync4(from.source, "utf8") : from.content;
3526
- const disk = existsSync5(abs) ? readFileSync4(abs, "utf8") : null;
3755
+ const disk = existsSync6(abs) ? readFileSync4(abs, "utf8") : null;
3527
3756
  const j4 = judge({
3528
3757
  op: "write",
3529
3758
  kind: "harness",
@@ -3585,8 +3814,8 @@ function createInstallWriter(args) {
3585
3814
  const file = SHARED_FILES[path];
3586
3815
  if (file === void 0) throw new Error(`install: ${path} is not a shared file`);
3587
3816
  const adapter = ADAPTERS[file.adapter];
3588
- const abs = join6(projectDir, path);
3589
- const disk = existsSync5(abs) ? readFileSync4(abs, "utf8") : null;
3817
+ const abs = join7(projectDir, path);
3818
+ const disk = existsSync6(abs) ? readFileSync4(abs, "utf8") : null;
3590
3819
  const own = new Map(
3591
3820
  (previousLog?.portions ?? []).filter((p2) => p2.path === path).map((p2) => [p2.key, p2.sha256])
3592
3821
  );
@@ -3742,21 +3971,36 @@ function thisRunExclusions(spec, previous, accepts) {
3742
3971
  ]);
3743
3972
  return { spec: specWithExclusions(spec, excluded), excluded };
3744
3973
  }
3745
- function inheritScanned(previous) {
3974
+ function inheritScanned(previous, projectDir) {
3746
3975
  if (previous === null) return { policyFiles: [], skillFiles: [] };
3747
3976
  const policyFiles = previous.policyFiles ?? [];
3748
3977
  const skillFiles = previous.skillFiles ?? [];
3749
3978
  if (previous.records === "writer")
3750
3979
  return { policyFiles: [...policyFiles], skillFiles: [...skillFiles] };
3751
3980
  const owned = (full) => recorded(previous, full).state !== "none";
3981
+ const kept = policyFiles.filter((f) => owned(`${CLAUDE_DIR2}${f.path}`));
3752
3982
  return {
3753
- policyFiles: policyFiles.filter((f) => owned(`${CLAUDE_DIR2}${f.path}`)),
3983
+ policyFiles: projectDir === void 0 ? kept : [...kept, ...retiredClaims(previous, projectDir, kept)],
3754
3984
  skillFiles: skillFiles.filter((f) => owned(`${SKILLS_DIR2}${f.path}`))
3755
3985
  };
3756
3986
  }
3987
+ function retiredClaims(previous, projectDir, kept) {
3988
+ const have = new Set(kept.map((f) => f.path));
3989
+ const out = [];
3990
+ for (const p2 of RETIRED_PATHS) {
3991
+ if (have.has(p2) || recorded(previous, `${CLAUDE_DIR2}${p2}`).state === "none") continue;
3992
+ const abs = join7(projectDir, CLAUDE_DIR2, p2);
3993
+ try {
3994
+ if (!statSync(abs).isFile()) continue;
3995
+ out.push({ path: p2, sha256: hashContent(readFileSync4(abs, "utf8")) });
3996
+ } catch {
3997
+ }
3998
+ }
3999
+ return out;
4000
+ }
3757
4001
  function composeWriterLog(args) {
3758
4002
  const { base, previous, ledger } = args;
3759
- const inherited = inheritScanned(previous);
4003
+ const inherited = inheritScanned(previous, args.projectDir);
3760
4004
  const policyFiles = mergeExternalFiles(inherited.policyFiles, ledger.policyFiles);
3761
4005
  const skillFiles = mergeExternalFiles(inherited.skillFiles, ledger.skillFiles);
3762
4006
  const externalFiles = mergeExternalFiles(previous?.externalFiles, [
@@ -3852,7 +4096,7 @@ function legacySettingsSeed(text, render, projectDir) {
3852
4096
  } catch {
3853
4097
  return seed;
3854
4098
  }
3855
- const claudeDir = join6(projectDir, ".claude");
4099
+ const claudeDir = join7(projectDir, ".claude");
3856
4100
  const candidates = new Set([...render.keys()].filter((k2) => k2.startsWith("hooks.")));
3857
4101
  const hooks = isObject2(root) && isObject2(root.hooks) ? root.hooks : {};
3858
4102
  for (const [event, groups] of Object.entries(hooks)) {
@@ -3965,13 +4209,13 @@ function legacyGitignoreSeed(text, render, previous) {
3965
4209
 
3966
4210
  // src/installer.ts
3967
4211
  init_esm_shims();
3968
- import { chmodSync as chmodSync3, existsSync as existsSync21, readdirSync as readdirSync4, readFileSync as readFileSync22, writeFileSync as writeFileSync8 } from "fs";
3969
- import { basename as basename4, join as join29, relative as relative3, resolve, sep as sep4 } from "path";
4212
+ import { chmodSync as chmodSync3, existsSync as existsSync22, readdirSync as readdirSync4, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "fs";
4213
+ import { basename as basename4, join as join30, relative as relative3, resolve, sep as sep4 } from "path";
3970
4214
 
3971
4215
  // src/anchor-seed.ts
3972
4216
  init_esm_shims();
3973
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
3974
- import { join as join7 } from "path";
4217
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
4218
+ import { join as join8 } from "path";
3975
4219
 
3976
4220
  // src/agents-md-render.ts
3977
4221
  init_esm_shims();
@@ -4055,7 +4299,7 @@ ${CODEX_SIZE_NOTE}` : RULES_NOTE : "",
4055
4299
 
4056
4300
  // src/anchor-seed.ts
4057
4301
  function readIfExists(path) {
4058
- if (!existsSync6(path)) return null;
4302
+ if (!existsSync7(path)) return null;
4059
4303
  try {
4060
4304
  return readFileSync5(path, "utf8");
4061
4305
  } catch {
@@ -4078,7 +4322,7 @@ function compose(body, into) {
4078
4322
  ${trimmed.join("\n")}`;
4079
4323
  }
4080
4324
  function seedAgentsMdProjectContext(projectDir) {
4081
- const root = readIfExists(join7(projectDir, "CLAUDE.md"));
4325
+ const root = readIfExists(join8(projectDir, "CLAUDE.md"));
4082
4326
  if (root === null) return null;
4083
4327
  const withoutImport = stripHarnessImport(root) ?? root;
4084
4328
  const body = dropHarnessQuoteBlocks(dropTitle(withoutImport.split("\n")));
@@ -4086,9 +4330,9 @@ function seedAgentsMdProjectContext(projectDir) {
4086
4330
  return compose(body, "agents-md");
4087
4331
  }
4088
4332
  function seedRootClaudeProjectContext(projectDir, harnessRoot) {
4089
- const agents = readIfExists(join7(projectDir, "AGENTS.md"));
4333
+ const agents = readIfExists(join8(projectDir, "AGENTS.md"));
4090
4334
  if (agents === null) return null;
4091
- const template = readIfExists(join7(harnessRoot, AGENTS_MD_TEMPLATE));
4335
+ const template = readIfExists(join8(harnessRoot, AGENTS_MD_TEMPLATE));
4092
4336
  if (template === null) return null;
4093
4337
  const installer = projectContextInstallerLines({ existing: agents, template });
4094
4338
  if (installer === null) return null;
@@ -4099,8 +4343,8 @@ function seedRootClaudeProjectContext(projectDir, harnessRoot) {
4099
4343
 
4100
4344
  // src/ci-scaffold.ts
4101
4345
  init_esm_shims();
4102
- import { existsSync as existsSync7 } from "fs";
4103
- import { join as join8 } from "path";
4346
+ import { existsSync as existsSync8 } from "fs";
4347
+ import { join as join9 } from "path";
4104
4348
  var PYTHON_TRACKS = "data|csr-fastapi|full";
4105
4349
  var VARIANTS = [
4106
4350
  {
@@ -4124,12 +4368,12 @@ function installCiScaffold(ctx) {
4124
4368
  for (const v2 of VARIANTS) {
4125
4369
  if (!v2.applies(ctx.tracks)) continue;
4126
4370
  const relTarget = `.github/workflows/${v2.target}`;
4127
- const target = join8(ctx.projectDir, relTarget);
4128
- if (existsSync7(target)) {
4371
+ const target = join9(ctx.projectDir, relTarget);
4372
+ if (existsSync8(target)) {
4129
4373
  report.skippedExisting.push(relTarget);
4130
4374
  continue;
4131
4375
  }
4132
- copyFile(join8(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
4376
+ copyFile(join9(ctx.harnessRoot, "templates/github-workflows", v2.source), target);
4133
4377
  report.written.push(relTarget);
4134
4378
  }
4135
4379
  return report;
@@ -4137,19 +4381,18 @@ function installCiScaffold(ctx) {
4137
4381
 
4138
4382
  // src/cli-transforms.ts
4139
4383
  init_esm_shims();
4140
- import { existsSync as existsSync16, readFileSync as readFileSync16 } from "fs";
4141
- import { join as join21 } from "path";
4384
+ import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
4385
+ import { join as join22 } from "path";
4142
4386
 
4143
4387
  // src/agents-md-transform.ts
4144
4388
  init_esm_shims();
4145
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
4146
- import { basename, join as join13 } from "path";
4389
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
4390
+ import { basename, join as join14 } from "path";
4147
4391
 
4148
4392
  // src/agents-skill-targets.ts
4149
4393
  init_esm_shims();
4150
- import { join as join9, sep as sep2 } from "path";
4151
- function bundledSkillTargets(projectDir) {
4152
- const log = readInstallLog(projectDir);
4394
+ import { join as join10, sep as sep2 } from "path";
4395
+ function bundledSkillTargets(projectDir, log = readInstallLog(projectDir)) {
4153
4396
  if (log === null) return [];
4154
4397
  const tracks = log.spec.tracks.filter(
4155
4398
  (t) => TRACKS.includes(t)
@@ -4162,7 +4405,7 @@ function bundledSkillTargets(projectDir) {
4162
4405
  }
4163
4406
  function allowSkillCreation(writer, projectDir, ids) {
4164
4407
  if (ids.length === 0) return writer;
4165
- const roots = ids.map((id) => `${join9(projectDir, ".agents", "skills", id)}${sep2}`);
4408
+ const roots = ids.map((id) => `${join10(projectDir, ".agents", "skills", id)}${sep2}`);
4166
4409
  return {
4167
4410
  write(absPath, content, opts) {
4168
4411
  const inTarget = roots.some((root) => absPath.startsWith(root));
@@ -4183,24 +4426,24 @@ function agentsSkillSlot(params) {
4183
4426
 
4184
4427
  // src/owned-write.ts
4185
4428
  init_esm_shims();
4186
- import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
4429
+ import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
4187
4430
  import { dirname as dirname2, relative as relative2, sep as sep3 } from "path";
4188
4431
 
4189
4432
  // src/foreign-slot.ts
4190
4433
  init_esm_shims();
4191
- import { lstatSync, realpathSync, statSync } from "fs";
4192
- import { join as join10 } from "path";
4434
+ import { lstatSync, realpathSync, statSync as statSync2 } from "fs";
4435
+ import { join as join11 } from "path";
4193
4436
  var SKILL_SLOT = /^((?:\.claude|\.agents)\/skills\/[^/]+)(?:\/|$)/;
4194
4437
  function occupiedByNonDirectory(path) {
4195
4438
  const stat = lstatSync(path, { throwIfNoEntry: false });
4196
4439
  return stat !== void 0 && !stat.isDirectory();
4197
4440
  }
4198
4441
  function linksToProjectSharedSkill(projectDir, id) {
4199
- const slot = join10(projectDir, ".claude/skills", id);
4442
+ const slot = join11(projectDir, ".claude/skills", id);
4200
4443
  if (lstatSync(slot, { throwIfNoEntry: false })?.isSymbolicLink() !== true) return false;
4201
4444
  try {
4202
4445
  const real = realpathSync(slot);
4203
- return real === join10(realpathSync(projectDir), ".agents/skills", id) && statSync(real).isDirectory();
4446
+ return real === join11(realpathSync(projectDir), ".agents/skills", id) && statSync2(real).isDirectory();
4204
4447
  } catch {
4205
4448
  return false;
4206
4449
  }
@@ -4208,15 +4451,15 @@ function linksToProjectSharedSkill(projectDir, id) {
4208
4451
  function foreignOwnedTarget(projectDir, rel) {
4209
4452
  const slot = SKILL_SLOT.exec(rel)?.[1];
4210
4453
  if (slot === void 0) return null;
4211
- if (occupiedByNonDirectory(join10(projectDir, slot))) return slot;
4454
+ if (occupiedByNonDirectory(join11(projectDir, slot))) return slot;
4212
4455
  if (rel !== slot) {
4213
4456
  const parts = rel.slice(slot.length + 1).split("/");
4214
4457
  let cursor = slot;
4215
4458
  for (const part of parts.slice(0, -1)) {
4216
4459
  cursor = `${cursor}/${part}`;
4217
- if (occupiedByNonDirectory(join10(projectDir, cursor))) return cursor;
4460
+ if (occupiedByNonDirectory(join11(projectDir, cursor))) return cursor;
4218
4461
  }
4219
- const leaf = lstatSync(join10(projectDir, rel), { throwIfNoEntry: false });
4462
+ const leaf = lstatSync(join11(projectDir, rel), { throwIfNoEntry: false });
4220
4463
  if (leaf !== void 0 && !leaf.isFile()) return rel;
4221
4464
  }
4222
4465
  return null;
@@ -4224,13 +4467,18 @@ function foreignOwnedTarget(projectDir, rel) {
4224
4467
 
4225
4468
  // src/owned-write.ts
4226
4469
  function createOwnedWriter(projectDir, baseline, options = {}) {
4227
- const { now = /* @__PURE__ */ new Date(), refreshOnly = false, journal } = options;
4470
+ const { now = /* @__PURE__ */ new Date(), refreshOnly = false, journal, owned = () => false } = options;
4228
4471
  const written = /* @__PURE__ */ new Map();
4229
4472
  const backedUp = [];
4230
4473
  const backupPaths = [];
4231
4474
  const foreignOwned = [];
4232
4475
  const restored = [];
4476
+ const displaced = /* @__PURE__ */ new Map();
4233
4477
  const outside = createOutsideGuard(projectDir);
4478
+ const displace = (f) => {
4479
+ displaced.set(f.path, f);
4480
+ journal?.displaced?.(f);
4481
+ };
4234
4482
  let updated = 0;
4235
4483
  return {
4236
4484
  write(absPath, content, opts) {
@@ -4242,7 +4490,7 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
4242
4490
  }
4243
4491
  if (outside.skip(absPath)) return false;
4244
4492
  const digest = hashContent(content);
4245
- if (!existsSync8(absPath)) {
4493
+ if (!existsSync9(absPath)) {
4246
4494
  const recordedPath = baseline.has(rel);
4247
4495
  if (refreshOnly && !opts?.createInRefresh && !recordedPath) return false;
4248
4496
  mkdirSync2(dirname2(absPath), { recursive: true });
@@ -4251,12 +4499,20 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
4251
4499
  if (refreshOnly && recordedPath) restored.push(rel);
4252
4500
  } else {
4253
4501
  const current = readFileSync6(absPath, "utf8");
4502
+ const firstContact = !baseline.has(rel) && !owned(rel);
4503
+ if (current === content && firstContact) {
4504
+ displace({ path: rel, change: "displaced", notes: [] });
4505
+ return true;
4506
+ }
4254
4507
  if (current !== content) {
4255
4508
  if (!isHarnessOwned(baseline, rel, current)) {
4256
4509
  const backup = backupFile(absPath, now);
4257
4510
  backupPaths.push(backup);
4258
- backedUp.push(rel);
4259
4511
  journal?.backup(backup);
4512
+ if (firstContact) {
4513
+ const backupRel = relative2(projectDir, backup).split(sep3).join("/");
4514
+ displace({ path: rel, change: "displaced", notes: [backupRel] });
4515
+ } else backedUp.push(rel);
4260
4516
  }
4261
4517
  writeFileSync4(absPath, content);
4262
4518
  updated++;
@@ -4276,7 +4532,8 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
4276
4532
  backedUp: [...backedUp],
4277
4533
  backupPaths: [...backupPaths],
4278
4534
  updated,
4279
- restored: [...restored]
4535
+ restored: [...restored],
4536
+ displaced: [...displaced.values()]
4280
4537
  };
4281
4538
  }
4282
4539
  };
@@ -4284,8 +4541,8 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
4284
4541
 
4285
4542
  // src/rules-port.ts
4286
4543
  init_esm_shims();
4287
- import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync7 } from "fs";
4288
- import { join as join11 } from "path";
4544
+ import { existsSync as existsSync10, readdirSync, readFileSync as readFileSync7 } from "fs";
4545
+ import { join as join12 } from "path";
4289
4546
  function claudePaths(source) {
4290
4547
  if (!source.startsWith("---\n")) return [];
4291
4548
  const end = source.indexOf("\n---", 4);
@@ -4313,16 +4570,16 @@ function stripClaudeFrontmatter(source) {
4313
4570
  function portRules(harnessRoot, ruleNames) {
4314
4571
  const ported = [];
4315
4572
  for (const name of ruleNames) {
4316
- const src = join11(harnessRoot, "templates/rules", `${name}.md`);
4317
- if (!existsSync9(src)) continue;
4573
+ const src = join12(harnessRoot, "templates/rules", `${name}.md`);
4574
+ if (!existsSync10(src)) continue;
4318
4575
  const source = readFileSync7(src, "utf8");
4319
4576
  ported.push({ name, body: stripClaudeFrontmatter(source), paths: claudePaths(source) });
4320
4577
  }
4321
4578
  return ported;
4322
4579
  }
4323
4580
  function ruleHeadings(harnessRoot) {
4324
- const dir = join11(harnessRoot, "templates/rules");
4325
- if (!existsSync9(dir)) return /* @__PURE__ */ new Set();
4581
+ const dir = join12(harnessRoot, "templates/rules");
4582
+ if (!existsSync10(dir)) return /* @__PURE__ */ new Set();
4326
4583
  const names = readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => f.slice(0, -3));
4327
4584
  const block = renderRulesBlock(portRules(harnessRoot, names));
4328
4585
  return new Set(
@@ -4336,8 +4593,8 @@ function renderRulesBlock(rules) {
4336
4593
 
4337
4594
  // src/shared-write.ts
4338
4595
  init_esm_shims();
4339
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync, statSync as statSync2, writeFileSync as writeFileSync5 } from "fs";
4340
- import { dirname as dirname3, join as join12 } from "path";
4596
+ import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
4597
+ import { dirname as dirname3, join as join13 } from "path";
4341
4598
  function toPortions(path, portions) {
4342
4599
  const adapter = adapterFor(path);
4343
4600
  if (adapter === null) throw new Error(`shared-write: ${path} is not a shared file`);
@@ -4352,8 +4609,8 @@ function writeShared(params) {
4352
4609
  const { projectDir, path, render, record, baseline, writer, refreshOnly } = params;
4353
4610
  const adapter = adapterFor(path);
4354
4611
  if (adapter === null) throw new Error(`shared-write: ${path} is not a shared file`);
4355
- const abs = join12(projectDir, path);
4356
- const onDisk = existsSync10(abs) ? readFileSync8(abs, "utf8") : null;
4612
+ const abs = join13(projectDir, path);
4613
+ const onDisk = existsSync11(abs) ? readFileSync8(abs, "utf8") : null;
4357
4614
  const recorded2 = recordedFor(path, record);
4358
4615
  const excluded = excludedKeys(path, record.excluded ?? []);
4359
4616
  const result = (action2, rest = {}) => ({
@@ -4472,8 +4729,8 @@ function agentsMdModel(disk, recordedByHarness, recordedBlock = false) {
4472
4729
  return "block";
4473
4730
  }
4474
4731
  function writeAgentsMd(params) {
4475
- const abs = join12(params.projectDir, AGENTS_MD);
4476
- const disk = existsSync10(abs) ? readFileSync8(abs, "utf8") : null;
4732
+ const abs = join13(params.projectDir, AGENTS_MD);
4733
+ const disk = existsSync11(abs) ? readFileSync8(abs, "utf8") : null;
4477
4734
  const recordedBlock = (params.record.portions ?? []).some((p2) => p2.path === AGENTS_MD);
4478
4735
  if (agentsMdModel(disk, params.baseline.has(AGENTS_MD), recordedBlock) === "sections") {
4479
4736
  params.writer.write(
@@ -4526,8 +4783,8 @@ function stripShared(params) {
4526
4783
  portions: [],
4527
4784
  ...rest
4528
4785
  });
4529
- const abs = join12(projectDir, path);
4530
- if (!existsSync10(abs) || !statSync2(abs).isFile()) return out("none");
4786
+ const abs = join13(projectDir, path);
4787
+ if (!existsSync11(abs) || !statSync3(abs).isFile()) return out("none");
4531
4788
  const disk = readFileSync8(abs, "utf8");
4532
4789
  const recorded2 = new Map(
4533
4790
  params.portions.filter((p2) => p2.path === path).map((p2) => [p2.key, p2.sha256])
@@ -4579,18 +4836,19 @@ function runAgentsMdTransform(params) {
4579
4836
  const clis = agentsMdClis(params.clis);
4580
4837
  const writer = createOwnedWriter(projectDir, baseline, {
4581
4838
  refreshOnly,
4582
- ...params.journal === void 0 ? {} : { journal: params.journal }
4839
+ ...params.journal === void 0 ? {} : { journal: params.journal },
4840
+ ...params.owned === void 0 ? {} : { owned: params.owned }
4583
4841
  });
4584
- const claudeMd = readRequired(join13(harnessRoot, "templates/CLAUDE.md"));
4585
- const template = readRequired(join13(harnessRoot, AGENTS_MD_TEMPLATE));
4842
+ const claudeMd = readRequired(join14(harnessRoot, "templates/CLAUDE.md"));
4843
+ const template = readRequired(join14(harnessRoot, AGENTS_MD_TEMPLATE));
4586
4844
  const { skillIds } = agentsSkillSlot({
4587
4845
  projectDir,
4588
4846
  selectedInternalSkills: params.selectedInternalSkills,
4589
4847
  refreshOnly,
4590
4848
  writer
4591
4849
  });
4592
- const agentsMdPath = join13(projectDir, "AGENTS.md");
4593
- const exists = existsSync11(agentsMdPath);
4850
+ const agentsMdPath = join14(projectDir, "AGENTS.md");
4851
+ const exists = existsSync12(agentsMdPath);
4594
4852
  const creates = !exists && (!refreshOnly || params.installedByRecord === true);
4595
4853
  const legacy = params.legacyContext ?? null;
4596
4854
  const fromRoot = creates ? seedAgentsMdProjectContext(projectDir) : null;
@@ -4632,13 +4890,13 @@ function runAgentsMdTransform(params) {
4632
4890
  };
4633
4891
  }
4634
4892
  function seededInto(path, before, seed, from) {
4635
- if (seed === null || !existsSync11(path)) return null;
4893
+ if (seed === null || !existsSync12(path)) return null;
4636
4894
  const after = readFileSync9(path, "utf8");
4637
4895
  const body = renameSlashes(seed.slice(seed.indexOf("\n\n") + 2));
4638
4896
  return after !== before && after.includes(body) ? from : null;
4639
4897
  }
4640
4898
  function readRequired(path) {
4641
- if (!existsSync11(path)) {
4899
+ if (!existsSync12(path)) {
4642
4900
  throw new Error(`AGENTS.md transform: required source missing: ${path}`);
4643
4901
  }
4644
4902
  return readFileSync9(path, "utf8");
@@ -4647,7 +4905,7 @@ function readRequired(path) {
4647
4905
  // src/antigravity/retire-anchor.ts
4648
4906
  init_esm_shims();
4649
4907
  import { lstatSync as lstatSync2, readFileSync as readFileSync10, rmSync as rmSync2 } from "fs";
4650
- import { join as join14 } from "path";
4908
+ import { join as join15 } from "path";
4651
4909
  var LEGACY_ANTIGRAVITY_ANCHOR_SECTIONS = ["Project Context", "Project Rules", "Protected Files"];
4652
4910
  function anchorRecord(previous) {
4653
4911
  return previous.find((f) => f.path === LEGACY_ANTIGRAVITY_ANCHOR) ?? null;
@@ -4675,7 +4933,7 @@ function legacyContextLines(text) {
4675
4933
  }
4676
4934
  function readLegacyAntigravityContext(projectDir, previous) {
4677
4935
  if (anchorRecord(previous) === null) return null;
4678
- const text = readRegular(join14(projectDir, LEGACY_ANTIGRAVITY_ANCHOR));
4936
+ const text = readRegular(join15(projectDir, LEGACY_ANTIGRAVITY_ANCHOR));
4679
4937
  if (text === null) return null;
4680
4938
  const lines2 = legacyContextLines(text);
4681
4939
  if (lines2 === null) return null;
@@ -4699,7 +4957,7 @@ function retireAntigravityAnchor(args) {
4699
4957
  const rec = anchorRecord(args.previous);
4700
4958
  if (rec === null) return null;
4701
4959
  const path = LEGACY_ANTIGRAVITY_ANCHOR;
4702
- const abs = join14(args.projectDir, path);
4960
+ const abs = join15(args.projectDir, path);
4703
4961
  const left = (why) => ({
4704
4962
  path,
4705
4963
  action: "left",
@@ -4737,12 +4995,12 @@ function relativeBackup(projectDir, backup) {
4737
4995
 
4738
4996
  // src/antigravity/transform.ts
4739
4997
  init_esm_shims();
4740
- import { join as join16 } from "path";
4998
+ import { join as join17 } from "path";
4741
4999
 
4742
5000
  // src/codex/skills.ts
4743
5001
  init_esm_shims();
4744
- import { chmodSync, existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
4745
- import { join as join15 } from "path";
5002
+ import { chmodSync, existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
5003
+ import { join as join16 } from "path";
4746
5004
  function renderBundledSkill(source) {
4747
5005
  const trimmed = source.trimEnd();
4748
5006
  const lines2 = trimmed.split(/\r?\n/);
@@ -4774,13 +5032,13 @@ function writeBundledSkillDirs(params) {
4774
5032
  const { harnessRoot, projectDir, skillIds, writer } = params;
4775
5033
  const written = [];
4776
5034
  for (const id of skillIds) {
4777
- const srcDir = join15(harnessRoot, "templates/skills", id);
4778
- const srcSkill = join15(srcDir, "SKILL.md");
4779
- if (!existsSync12(srcSkill)) {
5035
+ const srcDir = join16(harnessRoot, "templates/skills", id);
5036
+ const srcSkill = join16(srcDir, "SKILL.md");
5037
+ if (!existsSync13(srcSkill)) {
4780
5038
  continue;
4781
5039
  }
4782
- const targetDir = join15(projectDir, ".agents", "skills", id);
4783
- const targetSkill = join15(targetDir, "SKILL.md");
5040
+ const targetDir = join16(projectDir, ".agents", "skills", id);
5041
+ const targetSkill = join16(targetDir, "SKILL.md");
4784
5042
  if (!writer.write(targetSkill, renderBundledSkill(readFileSync11(srcSkill, "utf8")))) {
4785
5043
  continue;
4786
5044
  }
@@ -4789,10 +5047,10 @@ function writeBundledSkillDirs(params) {
4789
5047
  if (rel === "SKILL.md") continue;
4790
5048
  if (rel.split("/").some((segment2) => segment2.startsWith("."))) continue;
4791
5049
  const segments = rel.split("/");
4792
- const raw = readFileSync11(join15(srcDir, ...segments), "utf8");
5050
+ const raw = readFileSync11(join16(srcDir, ...segments), "utf8");
4793
5051
  const ported = rel.endsWith(".md") || rel.endsWith(".sh") ? `${portBody(raw)}
4794
5052
  ` : raw;
4795
- const target = join15(targetDir, ...segments);
5053
+ const target = join16(targetDir, ...segments);
4796
5054
  if (!writer.write(target, ported, { createInRefresh: true })) continue;
4797
5055
  if (rel.endsWith(".sh")) chmodSync(target, 493);
4798
5056
  written.push(target);
@@ -4822,7 +5080,8 @@ function runAntigravityTransform(params) {
4822
5080
  } = params;
4823
5081
  const writer = createOwnedWriter(projectDir, baseline, {
4824
5082
  refreshOnly: refreshOnly ?? false,
4825
- ...params.journal === void 0 ? {} : { journal: params.journal }
5083
+ ...params.journal === void 0 ? {} : { journal: params.journal },
5084
+ ...params.owned === void 0 ? {} : { owned: params.owned }
4826
5085
  });
4827
5086
  const { skillIds, skillWriter } = agentsSkillSlot({
4828
5087
  projectDir,
@@ -4833,7 +5092,7 @@ function runAntigravityTransform(params) {
4833
5092
  const antigravityInstalled = params.installedByRecord === true;
4834
5093
  const harnessRuleFiles = [];
4835
5094
  for (const rule of portRules(harnessRoot, rules)) {
4836
- const target = join16(projectDir, ".agents", "rules", `${rule.name}.md`);
5095
+ const target = join17(projectDir, ".agents", "rules", `${rule.name}.md`);
4837
5096
  const content = `${antigravityFrontmatter(rule)}${rule.body}
4838
5097
  `;
4839
5098
  if (!writer.write(target, content, { createInRefresh: antigravityInstalled })) {
@@ -4857,11 +5116,11 @@ function runAntigravityTransform(params) {
4857
5116
  // src/codex/opt-in.ts
4858
5117
  init_esm_shims();
4859
5118
  import { homedir } from "os";
4860
- import { join as join17 } from "path";
5119
+ import { join as join18 } from "path";
4861
5120
 
4862
5121
  // src/codex/trust-entry.ts
4863
5122
  init_esm_shims();
4864
- import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync12, realpathSync as realpathSync2, writeFileSync as writeFileSync6 } from "fs";
5123
+ import { existsSync as existsSync14, mkdirSync as mkdirSync4, readFileSync as readFileSync12, realpathSync as realpathSync2, writeFileSync as writeFileSync6 } from "fs";
4865
5124
  import { dirname as dirname4 } from "path";
4866
5125
  var UNSAFE_APPEND = "adding the entry would make this file unreadable TOML \u2014 left untouched";
4867
5126
  function trustKeysFor(projectDir) {
@@ -4904,7 +5163,7 @@ function registerTrustEntry(opts) {
4904
5163
  const keys = trustKeysFor(projectDir);
4905
5164
  const key = keys[0] ?? projectDir;
4906
5165
  try {
4907
- const existing = existsSync13(configPath) ? readFileSync12(configPath, "utf8") : "";
5166
+ const existing = existsSync14(configPath) ? readFileSync12(configPath, "utf8") : "";
4908
5167
  let parsed;
4909
5168
  try {
4910
5169
  parsed = parse(existing);
@@ -4955,8 +5214,8 @@ function firstLine(e2) {
4955
5214
 
4956
5215
  // src/codex/opt-in.ts
4957
5216
  function runCodexOptIn(ctx) {
4958
- const codexHome = ctx.codexHome ?? join17(homedir(), ".codex");
4959
- const configPath = join17(codexHome, "config.toml");
5217
+ const codexHome = ctx.codexHome ?? join18(homedir(), ".codex");
5218
+ const configPath = join18(codexHome, "config.toml");
4960
5219
  const result = registerTrustEntry({ configPath, projectDir: ctx.projectDir });
4961
5220
  return {
4962
5221
  trustEntry: {
@@ -4971,13 +5230,13 @@ function runCodexOptIn(ctx) {
4971
5230
 
4972
5231
  // src/codex/transform.ts
4973
5232
  init_esm_shims();
4974
- import { chmodSync as chmodSync2, existsSync as existsSync14, readFileSync as readFileSync14 } from "fs";
4975
- import { basename as basename2, join as join19 } from "path";
5233
+ import { chmodSync as chmodSync2, existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
5234
+ import { basename as basename2, join as join20 } from "path";
4976
5235
 
4977
5236
  // src/codex/config-toml.ts
4978
5237
  init_esm_shims();
4979
5238
  import { readFileSync as readFileSync13 } from "fs";
4980
- import { join as join18 } from "path";
5239
+ import { join as join19 } from "path";
4981
5240
  var TABLES_REGION = "tables";
4982
5241
  function configRegions(rendered) {
4983
5242
  const lines2 = rendered.split(/\r?\n/);
@@ -5022,7 +5281,7 @@ function retiredFeatureKeys(disk) {
5022
5281
  function retiredFeaturesLeftBehind(projectDir, r) {
5023
5282
  if (r.action !== "left" && !r.leftAsIs.includes(TABLES_REGION)) return [];
5024
5283
  try {
5025
- return retiredFeatureKeys(readFileSync13(join18(projectDir, ".codex/config.toml"), "utf8"));
5284
+ return retiredFeatureKeys(readFileSync13(join19(projectDir, ".codex/config.toml"), "utf8"));
5026
5285
  } catch {
5027
5286
  return [];
5028
5287
  }
@@ -5090,9 +5349,10 @@ function runCodexTransform(params) {
5090
5349
  } = params;
5091
5350
  const writer = createOwnedWriter(projectDir, baseline, {
5092
5351
  refreshOnly: refreshOnly ?? false,
5093
- ...params.journal === void 0 ? {} : { journal: params.journal }
5352
+ ...params.journal === void 0 ? {} : { journal: params.journal },
5353
+ ...params.owned === void 0 ? {} : { owned: params.owned }
5094
5354
  });
5095
- const configTemplate = readRequired2(join19(harnessRoot, "templates/codex/config.toml.template"));
5355
+ const configTemplate = readRequired2(join20(harnessRoot, "templates/codex/config.toml.template"));
5096
5356
  const projectName = basename2(projectDir);
5097
5357
  const { skillIds, skillWriter } = agentsSkillSlot({
5098
5358
  projectDir,
@@ -5100,7 +5360,7 @@ function runCodexTransform(params) {
5100
5360
  refreshOnly: refreshOnly ?? false,
5101
5361
  writer
5102
5362
  });
5103
- const configTomlPath = join19(projectDir, CONFIG_TOML);
5363
+ const configTomlPath = join20(projectDir, CONFIG_TOML);
5104
5364
  const configToml = writeConfigToml({
5105
5365
  projectDir,
5106
5366
  regions: configRegions(
@@ -5111,15 +5371,15 @@ function runCodexTransform(params) {
5111
5371
  record: shared2,
5112
5372
  refreshOnly: refreshOnly ?? false
5113
5373
  });
5114
- const hookDir = join19(projectDir, ".codex/hooks");
5374
+ const hookDir = join20(projectDir, ".codex/hooks");
5115
5375
  const hookFiles = [];
5116
5376
  for (const hook of HOOK_NAMES) {
5117
- const src = join19(harnessRoot, "templates/hooks", `${hook}.sh`);
5118
- if (!existsSync14(src)) {
5377
+ const src = join20(harnessRoot, "templates/hooks", `${hook}.sh`);
5378
+ if (!existsSync15(src)) {
5119
5379
  continue;
5120
5380
  }
5121
5381
  const ported = readFileSync14(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
5122
- const target = join19(hookDir, `${hook}.sh`);
5382
+ const target = join20(hookDir, `${hook}.sh`);
5123
5383
  if (!writer.write(target, ported)) continue;
5124
5384
  chmodSync2(target, 493);
5125
5385
  hookFiles.push(target);
@@ -5141,8 +5401,8 @@ function runCodexTransform(params) {
5141
5401
  var CONFIG_TOML = ".codex/config.toml";
5142
5402
  var LEGACY_CHANGED = "written by an earlier harness version and changed since \u2014 left as is; harness part not added";
5143
5403
  function writeConfigToml(args) {
5144
- const abs = join19(args.projectDir, CONFIG_TOML);
5145
- const disk = existsSync14(abs) ? readFileSync14(abs, "utf8") : null;
5404
+ const abs = join20(args.projectDir, CONFIG_TOML);
5405
+ const disk = existsSync15(abs) ? readFileSync14(abs, "utf8") : null;
5146
5406
  const regions = disk === null ? null : ADAPTERS["toml-region"].read(disk);
5147
5407
  if (disk !== null && regions?.size === 0 && isChangedLegacyConfig(disk, args.baseline.get(CONFIG_TOML))) {
5148
5408
  return {
@@ -5172,7 +5432,7 @@ function writeConfigToml(args) {
5172
5432
  });
5173
5433
  }
5174
5434
  function readRequired2(path) {
5175
- if (!existsSync14(path)) {
5435
+ if (!existsSync15(path)) {
5176
5436
  throw new Error(`Codex transform: required source missing: ${path}`);
5177
5437
  }
5178
5438
  return readFileSync14(path, "utf8");
@@ -5180,8 +5440,8 @@ function readRequired2(path) {
5180
5440
 
5181
5441
  // src/opencode/transform.ts
5182
5442
  init_esm_shims();
5183
- import { existsSync as existsSync15, readdirSync as readdirSync2, readFileSync as readFileSync15, unlinkSync } from "fs";
5184
- import { join as join20 } from "path";
5443
+ import { existsSync as existsSync16, readdirSync as readdirSync2, readFileSync as readFileSync15, unlinkSync } from "fs";
5444
+ import { join as join21 } from "path";
5185
5445
 
5186
5446
  // src/opencode/opencode-json.ts
5187
5447
  init_esm_shims();
@@ -5235,10 +5495,11 @@ function runOpencodeTransform(params) {
5235
5495
  } = params;
5236
5496
  const writer = createOwnedWriter(projectDir, baseline, {
5237
5497
  refreshOnly: refreshOnly ?? false,
5238
- ...params.journal === void 0 ? {} : { journal: params.journal }
5498
+ ...params.journal === void 0 ? {} : { journal: params.journal },
5499
+ ...params.owned === void 0 ? {} : { owned: params.owned }
5239
5500
  });
5240
5501
  const opencodeTemplate = readRequired3(
5241
- join20(harnessRoot, "templates/opencode/opencode.json.template")
5502
+ join21(harnessRoot, "templates/opencode/opencode.json.template")
5242
5503
  );
5243
5504
  const { skillIds, skillWriter } = agentsSkillSlot({
5244
5505
  projectDir,
@@ -5247,7 +5508,7 @@ function runOpencodeTransform(params) {
5247
5508
  writer
5248
5509
  });
5249
5510
  ensureDir(projectDir);
5250
- const opencodeJsonPath = join20(projectDir, OPENCODE_JSON);
5511
+ const opencodeJsonPath = join21(projectDir, OPENCODE_JSON);
5251
5512
  const render = renderOpencodeMcp(mcp);
5252
5513
  const opencodeJson = writeShared({
5253
5514
  projectDir,
@@ -5266,13 +5527,13 @@ function runOpencodeTransform(params) {
5266
5527
  writer: skillWriter
5267
5528
  });
5268
5529
  const retiredCommands = [];
5269
- const cmdDir = join20(projectDir, ".opencode/commands");
5270
- if (existsSync15(cmdDir)) {
5530
+ const cmdDir = join21(projectDir, ".opencode/commands");
5531
+ if (existsSync16(cmdDir)) {
5271
5532
  for (const entry of readdirSync2(cmdDir)) {
5272
5533
  if (!entry.endsWith(".md")) continue;
5273
5534
  const id = entry.slice(0, -3);
5274
- if (!existsSync15(join20(harnessRoot, "templates/skills", id, "SKILL.md"))) continue;
5275
- const victim = join20(cmdDir, entry);
5535
+ if (!existsSync16(join21(harnessRoot, "templates/skills", id, "SKILL.md"))) continue;
5536
+ const victim = join21(cmdDir, entry);
5276
5537
  if (writer.skipOutside(victim)) continue;
5277
5538
  try {
5278
5539
  backupFile(victim);
@@ -5292,7 +5553,7 @@ function runOpencodeTransform(params) {
5292
5553
  }
5293
5554
  var OPENCODE_JSON = "opencode.json";
5294
5555
  function readRequired3(path) {
5295
- if (!existsSync15(path)) {
5556
+ if (!existsSync16(path)) {
5296
5557
  throw new Error(`OpenCode transform: required source missing: ${path}`);
5297
5558
  }
5298
5559
  return readFileSync15(path, "utf8");
@@ -5317,7 +5578,7 @@ function trackDefaultMcpAssetIds(harnessRoot, tracks) {
5317
5578
  }
5318
5579
  function trackDefaultMcpNames(harnessRoot, tracks) {
5319
5580
  const { templateMcpPath } = mcpSources(harnessRoot);
5320
- const base = existsSync16(templateMcpPath) ? Object.keys(JSON.parse(readFileSync16(templateMcpPath, "utf8")).mcpServers) : [];
5581
+ const base = existsSync17(templateMcpPath) ? Object.keys(JSON.parse(readFileSync16(templateMcpPath, "utf8")).mcpServers) : [];
5321
5582
  return /* @__PURE__ */ new Set([
5322
5583
  ...base,
5323
5584
  ...trackRows(harnessRoot).filter((r) => anyTrack(tracks, r.pattern)).map((r) => r.name)
@@ -5325,7 +5586,7 @@ function trackDefaultMcpNames(harnessRoot, tracks) {
5325
5586
  }
5326
5587
  function trackRows(harnessRoot) {
5327
5588
  const { trackMapPath } = mcpSources(harnessRoot);
5328
- return existsSync16(trackMapPath) ? parseTrackMcpMap(readFileSync16(trackMapPath, "utf8")) : [];
5589
+ return existsSync17(trackMapPath) ? parseTrackMcpMap(readFileSync16(trackMapPath, "utf8")) : [];
5329
5590
  }
5330
5591
  function renderEverySelectableMcp(harnessRoot, tracks) {
5331
5592
  return composeMcpJson({
@@ -5345,8 +5606,8 @@ function adapterExcluded(harnessRoot, excluded) {
5345
5606
  }
5346
5607
  function mcpSources(harnessRoot) {
5347
5608
  return {
5348
- templateMcpPath: join21(harnessRoot, "templates/mcp.json"),
5349
- trackMapPath: join21(harnessRoot, "templates/track-mcp-map.tsv")
5609
+ templateMcpPath: join22(harnessRoot, "templates/mcp.json"),
5610
+ trackMapPath: join22(harnessRoot, "templates/track-mcp-map.tsv")
5350
5611
  };
5351
5612
  }
5352
5613
  var ALL_CLI_TARGETS = CLI_BASES;
@@ -5363,9 +5624,13 @@ function runCliTransforms(params) {
5363
5624
  codexTrust = false,
5364
5625
  shared: shared2 = {},
5365
5626
  recordedClis: recordedClis2 = [],
5366
- journal
5627
+ journal,
5628
+ owned
5367
5629
  } = params;
5368
- const journalParam = journal === void 0 ? {} : { journal };
5630
+ const writerParams = {
5631
+ ...journal === void 0 ? {} : { journal },
5632
+ ...owned === void 0 ? {} : { owned }
5633
+ };
5369
5634
  const legacyContext = readLegacyAntigravityContext(projectDir, previousExternal);
5370
5635
  const outside = createOutsideGuard(projectDir);
5371
5636
  const baseline = new Map(previousExternal.map((f) => [f.path, f.sha256]));
@@ -5373,6 +5638,7 @@ function runCliTransforms(params) {
5373
5638
  const externalBackups = [];
5374
5639
  const externalBackedUp = [];
5375
5640
  const externalForeignOwned = [];
5641
+ const displaced = /* @__PURE__ */ new Map();
5376
5642
  let externalOutside = [];
5377
5643
  let externalUpdated = 0;
5378
5644
  const restoredFiles = [];
@@ -5392,6 +5658,8 @@ function runCliTransforms(params) {
5392
5658
  }
5393
5659
  externalBackups.push(...report.ownership.backupPaths);
5394
5660
  externalBackedUp.push(...report.ownership.backedUp);
5661
+ for (const f of report.ownership.displaced ?? [])
5662
+ if (!displaced.has(f.path)) displaced.set(f.path, f);
5395
5663
  for (const f of report.ownership.foreignOwned) {
5396
5664
  if (!externalForeignOwned.includes(f)) externalForeignOwned.push(f);
5397
5665
  }
@@ -5411,7 +5679,7 @@ function runCliTransforms(params) {
5411
5679
  baseline,
5412
5680
  refreshOnly,
5413
5681
  shared: shared2,
5414
- ...journalParam
5682
+ ...writerParams
5415
5683
  });
5416
5684
  absorb(codex);
5417
5685
  absorbShared(codex, [codex.configToml]);
@@ -5431,7 +5699,7 @@ function runCliTransforms(params) {
5431
5699
  baseline,
5432
5700
  refreshOnly,
5433
5701
  shared: shared2,
5434
- ...journalParam
5702
+ ...writerParams
5435
5703
  });
5436
5704
  absorb(opencode);
5437
5705
  absorbShared(opencode, [opencode.opencodeJson]);
@@ -5447,7 +5715,7 @@ function runCliTransforms(params) {
5447
5715
  baseline,
5448
5716
  refreshOnly,
5449
5717
  installedByRecord: recordedClis2.includes("antigravity"),
5450
- ...journalParam
5718
+ ...writerParams
5451
5719
  });
5452
5720
  absorb(antigravity);
5453
5721
  journal?.done("antigravity");
@@ -5469,7 +5737,7 @@ function runCliTransforms(params) {
5469
5737
  installedByRecord: agentsSet.some((c3) => recordedClis2.includes(c3)),
5470
5738
  shared: shared2,
5471
5739
  legacyContext,
5472
- ...journalParam
5740
+ ...writerParams
5473
5741
  });
5474
5742
  absorb(agentsMd);
5475
5743
  absorbShared(agentsMd, [agentsMd.agentsMd.shared]);
@@ -5497,6 +5765,7 @@ function runCliTransforms(params) {
5497
5765
  externalBackups,
5498
5766
  externalUpdated,
5499
5767
  externalBackedUp,
5768
+ displaced: [...displaced.values()],
5500
5769
  externalForeignOwned,
5501
5770
  externalOutside,
5502
5771
  sharedFiles,
@@ -5508,8 +5777,8 @@ function runCliTransforms(params) {
5508
5777
 
5509
5778
  // src/excluded-still-there.ts
5510
5779
  init_esm_shims();
5511
- import { existsSync as existsSync17 } from "fs";
5512
- import { join as join22 } from "path";
5780
+ import { existsSync as existsSync18 } from "fs";
5781
+ import { join as join23 } from "path";
5513
5782
  function excludedStillThere(projectDir, excluded, baselineOnDisk, previousLog) {
5514
5783
  const out = [];
5515
5784
  const add = (id, catalog) => {
@@ -5522,7 +5791,7 @@ function excludedStillThere(projectDir, excluded, baselineOnDisk, previousLog) {
5522
5791
  for (const id of excluded) {
5523
5792
  if (INTERNAL_BUNDLED_SKILL_IDS.includes(id)) {
5524
5793
  const there = [".claude/skills", ".agents/skills"].some(
5525
- (d2) => existsSync17(join22(projectDir, d2, id))
5794
+ (d2) => existsSync18(join23(projectDir, d2, id))
5526
5795
  );
5527
5796
  if (there) add(id, false);
5528
5797
  } else {
@@ -5536,17 +5805,17 @@ function excludedStillThere(projectDir, excluded, baselineOnDisk, previousLog) {
5536
5805
  function assetStillThere(projectDir, asset) {
5537
5806
  if (asset.method !== "skill" || asset.scope === "global") return true;
5538
5807
  if (asset.files !== void 0)
5539
- return asset.files.some((f) => existsSync17(join22(projectDir, f.path)));
5808
+ return asset.files.some((f) => existsSync18(join23(projectDir, f.path)));
5540
5809
  const dir = asset.detail.skill ?? asset.id;
5541
- return [".claude/skills", ".agents/skills"].some((d2) => existsSync17(join22(projectDir, d2, dir)));
5810
+ return [".claude/skills", ".agents/skills"].some((d2) => existsSync18(join23(projectDir, d2, dir)));
5542
5811
  }
5543
5812
 
5544
5813
  // src/external-installer.ts
5545
5814
  init_esm_shims();
5546
5815
  import { spawnSync } from "child_process";
5547
- import { existsSync as existsSync18, readFileSync as readFileSync17 } from "fs";
5816
+ import { existsSync as existsSync19, mkdirSync as mkdirSync5, readFileSync as readFileSync17, writeFileSync as writeFileSync7 } from "fs";
5548
5817
  import { homedir as homedir2 } from "os";
5549
- import { join as join23 } from "path";
5818
+ import { dirname as dirname5, join as join24 } from "path";
5550
5819
  var SKILL_ROOTS = [".claude/skills", ".agents/skills"];
5551
5820
  function skillRootOf(cli) {
5552
5821
  return cli === "claude" ? ".claude/skills" : ".agents/skills";
@@ -5554,11 +5823,11 @@ function skillRootOf(cli) {
5554
5823
  function snapshotSkillRoots(projectDir) {
5555
5824
  const out = /* @__PURE__ */ new Map();
5556
5825
  for (const root of SKILL_ROOTS) {
5557
- if (outsideProjectTarget(projectDir, join23(projectDir, root)) !== null) continue;
5558
- for (const rel of listFilesRecursive(join23(projectDir, root))) {
5826
+ if (outsideProjectTarget(projectDir, join24(projectDir, root)) !== null) continue;
5827
+ for (const rel of listFilesRecursive(join24(projectDir, root))) {
5559
5828
  const path = `${root}/${rel}`;
5560
5829
  try {
5561
- out.set(path, hashContent(readFileSync17(join23(projectDir, path), "utf8")));
5830
+ out.set(path, hashContent(readFileSync17(join24(projectDir, path), "utf8")));
5562
5831
  } catch {
5563
5832
  }
5564
5833
  }
@@ -5635,7 +5904,7 @@ function installOne(asset, ctx) {
5635
5904
  return runSpawn(asset, ctx.spawn, "npx", buildSkillArgs(method, ctx.cli, ctx.scope), cwd);
5636
5905
  }
5637
5906
  const outside = SKILL_ROOTS.flatMap((root) => {
5638
- const hit = outsideProjectTarget(cwd, join23(cwd, root));
5907
+ const hit = outsideProjectTarget(cwd, join24(cwd, root));
5639
5908
  return hit === null ? [] : [{ root, target: hit.target }];
5640
5909
  });
5641
5910
  const outRoots = new Set(outside.map((o) => o.root));
@@ -5712,6 +5981,11 @@ function refreshExternalSkills(projectDir, deps = {}) {
5712
5981
  if (targets.length === 0) {
5713
5982
  return { ...none, notInCatalog, unknown: false };
5714
5983
  }
5984
+ const targetIds = new Set(targets.map((a) => a.id));
5985
+ const settle = holdSkillSlots(
5986
+ projectDir,
5987
+ log.assets.filter((a) => targetIds.has(a.id))
5988
+ );
5715
5989
  const report = (deps.run ?? runExternalInstall)(
5716
5990
  {
5717
5991
  tracks: log.spec.tracks,
@@ -5732,6 +6006,8 @@ function refreshExternalSkills(projectDir, deps = {}) {
5732
6006
  ...deps.warn ? { warn: deps.warn } : {}
5733
6007
  }
5734
6008
  );
6009
+ settle();
6010
+ recordRefreshedFiles(projectDir, log, report, deps.writeLog ?? writeInstallLog);
5735
6011
  return {
5736
6012
  attempted: report.attempted.length,
5737
6013
  refreshed: report.succeeded,
@@ -5740,6 +6016,65 @@ function refreshExternalSkills(projectDir, deps = {}) {
5740
6016
  unknown: false
5741
6017
  };
5742
6018
  }
6019
+ function holdSkillSlots(projectDir, assets) {
6020
+ const held = assets.flatMap((a) => holdSlotFiles(projectDir, a.files));
6021
+ return () => held.flatMap((h2) => settleHeld(h2) ?? []);
6022
+ }
6023
+ function holdSlotFiles(projectDir, files) {
6024
+ if (files === void 0) return [];
6025
+ const sha = new Map(files.map((f) => [f.path, f.sha256]));
6026
+ const slots = new Set(files.map((f) => f.path.split("/").slice(0, 3).join("/")));
6027
+ const out = [];
6028
+ for (const slot of slots) {
6029
+ if (outsideProjectTarget(projectDir, join24(projectDir, slot)) !== null) continue;
6030
+ for (const rel of listFilesRecursive(join24(projectDir, slot))) {
6031
+ const path = `${slot}/${rel}`;
6032
+ const abs = join24(projectDir, path);
6033
+ let content;
6034
+ try {
6035
+ content = readFileSync17(abs);
6036
+ } catch {
6037
+ continue;
6038
+ }
6039
+ const recordedSha = sha.get(path);
6040
+ if (recordedSha === void 0) out.push({ abs, content, recorded: false });
6041
+ else if (hashContent(content.toString("utf8")) !== recordedSha)
6042
+ out.push({ abs, content, recorded: true });
6043
+ }
6044
+ }
6045
+ return out;
6046
+ }
6047
+ function settleHeld(h2) {
6048
+ let now = null;
6049
+ try {
6050
+ now = readFileSync17(h2.abs);
6051
+ } catch {
6052
+ }
6053
+ if (now?.equals(h2.content)) return null;
6054
+ if (!h2.recorded && now === null) {
6055
+ mkdirSync5(dirname5(h2.abs), { recursive: true });
6056
+ writeFileSync7(h2.abs, h2.content);
6057
+ return null;
6058
+ }
6059
+ return backupContent(h2.abs, h2.content);
6060
+ }
6061
+ function recordRefreshedFiles(projectDir, log, report, writeLog) {
6062
+ const next = new Map(
6063
+ buildAssetEntries(report, log.scope, log.assets).filter((e2) => e2.files !== void 0).map((e2) => [e2.id, e2.files])
6064
+ );
6065
+ let changed = false;
6066
+ const assets = log.assets.map((a) => {
6067
+ const files = next.get(a.id);
6068
+ if (files === void 0 || JSON.stringify(files) === JSON.stringify(a.files)) return a;
6069
+ changed = true;
6070
+ return { ...a, files };
6071
+ });
6072
+ if (!changed) return;
6073
+ try {
6074
+ writeLog(projectDir, { ...log, assets });
6075
+ } catch {
6076
+ }
6077
+ }
5743
6078
  function buildSkillArgs(method, cli, scope) {
5744
6079
  const args = [skillsCliSpec(), "add", method.source];
5745
6080
  if (method.skill) {
@@ -5816,8 +6151,8 @@ function detectVersion(method, spawn, scope, projectDir) {
5816
6151
  case "plugin":
5817
6152
  return detectPluginVersion(method.pluginId, scope, projectDir);
5818
6153
  case "npm": {
5819
- const pkgJson = scope === "global" ? detectGlobalNpmPackageJsonPath(method.pkg, spawn) : join23(projectDir, "node_modules", method.pkg, "package.json");
5820
- if (!pkgJson || !existsSync18(pkgJson)) return void 0;
6154
+ const pkgJson = scope === "global" ? detectGlobalNpmPackageJsonPath(method.pkg, spawn) : join24(projectDir, "node_modules", method.pkg, "package.json");
6155
+ if (!pkgJson || !existsSync19(pkgJson)) return void 0;
5821
6156
  const parsed = JSON.parse(readFileSync17(pkgJson, "utf8"));
5822
6157
  return parsed.version;
5823
6158
  }
@@ -5830,8 +6165,8 @@ function detectVersion(method, spawn, scope, projectDir) {
5830
6165
  }
5831
6166
  function detectPluginVersion(pluginId, scope, projectDir) {
5832
6167
  const claudeScope = scope === "global" ? "user" : "project";
5833
- const path = join23(homedir2(), ".claude/plugins/installed_plugins.json");
5834
- if (!existsSync18(path)) return void 0;
6168
+ const path = join24(homedir2(), ".claude/plugins/installed_plugins.json");
6169
+ if (!existsSync19(path)) return void 0;
5835
6170
  const data = JSON.parse(readFileSync17(path, "utf8"));
5836
6171
  const entries = data.plugins?.[pluginId] ?? [];
5837
6172
  const matches = entries.filter(
@@ -5845,7 +6180,7 @@ function detectPluginVersion(pluginId, scope, projectDir) {
5845
6180
  }
5846
6181
  function detectGlobalNpmPackageJsonPath(pkg, spawn) {
5847
6182
  const npmRoot = getNpmGlobalRoot(spawn);
5848
- return npmRoot ? join23(npmRoot, pkg, "package.json") : void 0;
6183
+ return npmRoot ? join24(npmRoot, pkg, "package.json") : void 0;
5849
6184
  }
5850
6185
  var npmGlobalRootCache;
5851
6186
  function getNpmGlobalRoot(spawn) {
@@ -5865,7 +6200,7 @@ function getNpmGlobalRoot(spawn) {
5865
6200
  // src/key-ids.ts
5866
6201
  init_esm_shims();
5867
6202
  import { readFileSync as readFileSync18 } from "fs";
5868
- import { join as join24 } from "path";
6203
+ import { join as join25 } from "path";
5869
6204
  function renderedKeyIds(harnessRoot, input, clis) {
5870
6205
  const out = /* @__PURE__ */ new Set();
5871
6206
  const add = (path, key) => {
@@ -5888,7 +6223,7 @@ function renderedKeyIds(harnessRoot, input, clis) {
5888
6223
  for (const name of Object.keys(mcp.mcpServers)) add(".mcp.json", `mcpServers.${name}`);
5889
6224
  for (const key of gitignoreRender().keys()) add(".gitignore", key);
5890
6225
  if (clis.includes("claude")) {
5891
- const template = readFileSync18(join24(harnessRoot, "templates/settings.json"), "utf8");
6226
+ const template = readFileSync18(join25(harnessRoot, "templates/settings.json"), "utf8");
5892
6227
  for (const key of renderSettingsPortion(template, () => true).keys()) {
5893
6228
  add(".claude/settings.json", key);
5894
6229
  }
@@ -5929,7 +6264,7 @@ function withoutAccepts(harnessRoot, spec, previous) {
5929
6264
 
5930
6265
  // src/linked-skill-bodies.ts
5931
6266
  init_esm_shims();
5932
- import { join as join25 } from "path";
6267
+ import { join as join26 } from "path";
5933
6268
  function refreshLinkedSkillBodies(params) {
5934
6269
  const { harnessRoot, projectDir, ids, baseline, journal } = params;
5935
6270
  const bodyKey = (id) => `.agents/skills/${id}/SKILL.md`;
@@ -5940,10 +6275,11 @@ function refreshLinkedSkillBodies(params) {
5940
6275
  );
5941
6276
  const result = writer.result();
5942
6277
  return {
5943
- updated: ours.filter((id) => written.has(join25(projectDir, bodyKey(id)))),
6278
+ updated: ours.filter((id) => written.has(join26(projectDir, bodyKey(id)))),
5944
6279
  notOurs: ids.filter((id) => !baseline.has(bodyKey(id))),
5945
6280
  files: result.files,
5946
6281
  backupPaths: result.backupPaths,
6282
+ displaced: result.displaced ?? [],
5947
6283
  foreignOwned: result.foreignOwned,
5948
6284
  outside: result.outside ?? []
5949
6285
  };
@@ -5952,7 +6288,7 @@ function refreshLinkedSkillBodies(params) {
5952
6288
  // src/out-of-track.ts
5953
6289
  init_esm_shims();
5954
6290
  import { lstatSync as lstatSync3, readFileSync as readFileSync19, rmSync as rmSync3 } from "fs";
5955
- import { join as join26 } from "path";
6291
+ import { join as join27 } from "path";
5956
6292
  var AGENTS_RULE = /^\.agents\/rules\/([^/]+)\.md$/;
5957
6293
  var ANCHOR_RULE = "uzys-harness";
5958
6294
  function reclaimOutOfTrack(args) {
@@ -5972,7 +6308,7 @@ function reclaimOutOfTrack(args) {
5972
6308
  const name = AGENTS_RULE.exec(f.path)?.[1];
5973
6309
  if (name === void 0 || name === ANCHOR_RULE || rendered.has(name)) continue;
5974
6310
  if (isBaselineExcluded(`.claude/rules/${name}.md`, args.excluded)) continue;
5975
- const abs = join26(projectDir, f.path);
6311
+ const abs = join27(projectDir, f.path);
5976
6312
  if (args.outside.skip(abs)) continue;
5977
6313
  const stat = lstatSync3(abs, { throwIfNoEntry: false });
5978
6314
  if (stat !== void 0 && !stat.isFile()) continue;
@@ -6013,15 +6349,15 @@ function tracksBringing(rule) {
6013
6349
 
6014
6350
  // src/state.ts
6015
6351
  init_esm_shims();
6016
- import { existsSync as existsSync19, readFileSync as readFileSync20 } from "fs";
6017
- import { join as join27 } from "path";
6352
+ import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
6353
+ import { join as join28 } from "path";
6018
6354
  var META_FILE = ".claude/.installed-tracks";
6019
6355
  var LEGACY_ANCHOR_FILE = ".claude/CLAUDE.md";
6020
6356
  var TRACE_RULES = ["htmx", "nextjs", "data-analysis", "pyside6", "cli-development"].map(
6021
6357
  (r) => `.claude/rules/${r}.md`
6022
6358
  );
6023
6359
  function detectInstallState(projectDir) {
6024
- const hasClaudeDir = existsSync19(join27(projectDir, ".claude"));
6360
+ const hasClaudeDir = existsSync20(join28(projectDir, ".claude"));
6025
6361
  const read = readInstallLogStatus(projectDir);
6026
6362
  if (read.status === "ok") {
6027
6363
  return {
@@ -6041,17 +6377,17 @@ function tracksFromLog(tracks) {
6041
6377
  return [...new Set(tracks.filter(isTrack))].sort();
6042
6378
  }
6043
6379
  function collectTraces(projectDir) {
6044
- const at5 = (rel) => join27(projectDir, rel);
6045
- const contains = (rel, needle) => existsSync19(at5(rel)) && readFileSync20(at5(rel), "utf8").includes(needle);
6380
+ const at5 = (rel) => join28(projectDir, rel);
6381
+ const contains = (rel, needle) => existsSync20(at5(rel)) && readFileSync20(at5(rel), "utf8").includes(needle);
6046
6382
  const traces = [];
6047
- if (existsSync19(at5(META_FILE)))
6383
+ if (existsSync20(at5(META_FILE)))
6048
6384
  traces.push({ path: META_FILE, tracks: readMetafile(at5(META_FILE)) });
6049
- if (existsSync19(at5(HARNESS_ANCHOR_FILE))) traces.push({ path: HARNESS_ANCHOR_FILE });
6385
+ if (existsSync20(at5(HARNESS_ANCHOR_FILE))) traces.push({ path: HARNESS_ANCHOR_FILE });
6050
6386
  if (contains("CLAUDE.md", "<!-- uzys-harness:import:start -->"))
6051
6387
  traces.push({ path: "CLAUDE.md" });
6052
6388
  if (contains("AGENTS.md", "<!-- uzys-harness:")) traces.push({ path: "AGENTS.md" });
6053
6389
  for (const rel of [".agents/rules/uzys-harness.md", LEGACY_ANCHOR_FILE, ...TRACE_RULES]) {
6054
- if (existsSync19(at5(rel))) traces.push({ path: rel });
6390
+ if (existsSync20(at5(rel))) traces.push({ path: rel });
6055
6391
  }
6056
6392
  return traces;
6057
6393
  }
@@ -6100,17 +6436,17 @@ function reportNotInstalled(detected, projectDir, err) {
6100
6436
  init_esm_shims();
6101
6437
  import {
6102
6438
  copyFileSync,
6103
- existsSync as existsSync20,
6439
+ existsSync as existsSync21,
6104
6440
  lstatSync as lstatSync4,
6105
- mkdirSync as mkdirSync5,
6441
+ mkdirSync as mkdirSync6,
6106
6442
  readdirSync as readdirSync3,
6107
6443
  readFileSync as readFileSync21,
6108
6444
  rmdirSync,
6109
- statSync as statSync3,
6445
+ statSync as statSync4,
6110
6446
  unlinkSync as unlinkSync2,
6111
- writeFileSync as writeFileSync7
6447
+ writeFileSync as writeFileSync8
6112
6448
  } from "fs";
6113
- import { basename as basename3, dirname as dirname5, join as join28 } from "path";
6449
+ import { basename as basename3, dirname as dirname6, join as join29 } from "path";
6114
6450
  var LEGACY_ANCHOR_FILE2 = ".claude/CLAUDE.md";
6115
6451
  function buildUpdateSpec(projectDir, tracks, only) {
6116
6452
  const log = readInstallLog(projectDir);
@@ -6132,7 +6468,7 @@ function parseUpdateOnly(raw) {
6132
6468
  return { ok: true, groups: [...new Set(raw)] };
6133
6469
  }
6134
6470
  function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
6135
- const claudeDir = join28(projectDir, ".claude");
6471
+ const claudeDir = join29(projectDir, ".claude");
6136
6472
  const startedAt = Date.now();
6137
6473
  const backupsAtStart = new Set(
6138
6474
  collectRunBackups(projectDir, Number.NEGATIVE_INFINITY).map((b2) => b2.backup)
@@ -6174,33 +6510,63 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
6174
6510
  report.restored = fresh.restored;
6175
6511
  const logAtStart = readInstallLog(projectDir);
6176
6512
  const claudeManaged = logAtStart === null || installedClis(logAtStart).includes("claude");
6177
- report.claudeUnrecorded = !claudeManaged && logAtStart !== null && existsSync20(claudeDir) ? recordClaudeCommand(logAtStart, installedTracks(projectDir)) : null;
6513
+ const recordOf = recordedStateOf(logAtStart);
6514
+ const claudeDisplaced = [];
6515
+ report.claudeUnrecorded = !claudeManaged && logAtStart !== null && existsSync21(claudeDir) ? recordClaudeCommand(logAtStart, installedTracks(projectDir)) : null;
6178
6516
  const policyBase = policyBaseline(projectDir);
6179
6517
  for (const { dir, ext } of POLICY_DIRS) {
6180
6518
  if (!claudeManaged || !wants(dir === "hooks" ? "hooks" : "rules")) continue;
6181
- const target = join28(claudeDir, dir);
6182
- const source = join28(templatesDir, dir);
6519
+ const target = join29(claudeDir, dir);
6520
+ const source = join29(templatesDir, dir);
6183
6521
  const label = `.claude/${dir}`;
6184
- const ctx = { prefix: dir, baseline: policyBase, outside, excluded: excludedIds(logAtStart) };
6522
+ const ctx = {
6523
+ prefix: dir,
6524
+ baseline: policyBase,
6525
+ outside,
6526
+ excluded: excludedIds(logAtStart),
6527
+ // 0단계가 방금 깐 파일은 기록 전이지만 하네스 것이다 — 첫 접촉으로 읽지 않는다
6528
+ justWritten: new Set(fresh.installed),
6529
+ ...recordOf === void 0 ? {} : { recordOf }
6530
+ };
6185
6531
  const synced = updateDir(target, source, ext, ctx);
6186
6532
  report.updated[label] = synced.updated;
6187
6533
  report.policyBackedUp.push(...synced.backedUp);
6534
+ claudeDisplaced.push(...synced.displaced);
6188
6535
  report.pruned[label] = pruneOrphans(target, source, ext, ctx);
6189
6536
  }
6190
6537
  const syncedDirs = POLICY_DIRS.filter((d2) => wants(d2.dir === "hooks" ? "hooks" : "rules")).map(
6191
6538
  (d2) => d2.dir
6192
6539
  );
6193
6540
  if (claudeManaged && syncedDirs.length > 0)
6194
- refreshPolicyBaseline(projectDir, templatesDir, syncedDirs, outsidePaths(outside, ".claude/"));
6541
+ refreshPolicyBaseline(
6542
+ projectDir,
6543
+ templatesDir,
6544
+ syncedDirs,
6545
+ outsidePaths(outside, ".claude/"),
6546
+ notOursUnder(claudeDisplaced, ".claude/")
6547
+ );
6548
+ const skillTargets = logAtStart === null ? void 0 : recordedSkillIds(projectDir, logAtStart);
6195
6549
  const skillSync = claudeManaged && wants("skills") ? syncSkills(
6196
- join28(claudeDir, "skills"),
6197
- join28(templatesDir, "skills"),
6550
+ join29(claudeDir, "skills"),
6551
+ join29(templatesDir, "skills"),
6198
6552
  skillBaseline(projectDir),
6199
6553
  /* @__PURE__ */ new Date(),
6200
6554
  (relInSkills) => foreignOwnedTarget(projectDir, `.claude/skills/${relInSkills}`),
6201
6555
  outside,
6202
- excludedSkillOf(logAtStart)
6203
- ) : { updated: 0, backedUp: [], skippedLinks: [], foreignOwned: [], pruned: [] };
6556
+ excludedSkillOf(logAtStart),
6557
+ {
6558
+ ...recordOf === void 0 ? {} : { recordOf },
6559
+ ...skillTargets === void 0 ? {} : { targets: skillTargets }
6560
+ }
6561
+ ) : {
6562
+ updated: 0,
6563
+ backedUp: [],
6564
+ skippedLinks: [],
6565
+ foreignOwned: [],
6566
+ pruned: [],
6567
+ displaced: []
6568
+ };
6569
+ claudeDisplaced.push(...skillSync.displaced);
6204
6570
  if (wants("skills")) report.updated[".claude/skills"] = skillSync.updated;
6205
6571
  report.skillsBackedUp = skillSync.backedUp;
6206
6572
  report.skillsSkippedLinks = skillSync.skippedLinks;
@@ -6210,10 +6576,17 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
6210
6576
  report.restored.push(...newSkills.restored);
6211
6577
  const copiedSkills = [...newSkills.installed, ...newSkills.restored];
6212
6578
  if (claudeManaged && wants("skills"))
6213
- refreshSkillBaseline(projectDir, templatesDir, outsidePaths(outside, ".claude/skills/"));
6579
+ refreshSkillBaseline(
6580
+ projectDir,
6581
+ templatesDir,
6582
+ outsidePaths(outside, ".claude/skills/"),
6583
+ skillTargets,
6584
+ notOursUnder(claudeDisplaced, ".claude/skills/")
6585
+ );
6214
6586
  else if (claudeManaged && copiedSkills.length > 0)
6215
6587
  recordNewSkillBaseline(projectDir, templatesDir, copiedSkills);
6216
- if (existsSync20(claudeDir) && claudeManaged) {
6588
+ if (claudeDisplaced.length > 0) recordDisplaced(projectDir, claudeDisplaced);
6589
+ if (existsSync21(claudeDir) && claudeManaged) {
6217
6590
  if (wants("anchor")) syncHarnessAnchor(projectDir, templatesDir, report, outside);
6218
6591
  }
6219
6592
  const harnessMcp = renderHarnessMcp(harnessRoot, {
@@ -6232,14 +6605,18 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
6232
6605
  droppedKeys: legacyDroppedKeys(logAtStart),
6233
6606
  harnessMcp
6234
6607
  });
6235
- const settingsPath = join28(claudeDir, "settings.json");
6236
- if (claudeManaged && wants("hooks") && existsSync20(settingsPath) && !outside.skip(settingsPath)) {
6237
- report.staleHookRefs = cleanStaleHookRefs(
6608
+ const settingsPath = join29(claudeDir, "settings.json");
6609
+ if (claudeManaged && wants("hooks") && existsSync21(settingsPath) && !outside.skip(settingsPath)) {
6610
+ report.retiredHookRefsRemoved = removeRetiredHookRefs(
6611
+ settingsPath,
6612
+ claudeDir,
6613
+ RETIRED_HOOK_WIRING_REFS
6614
+ );
6615
+ report.staleHookRefs = findStaleHookRefs(
6238
6616
  settingsPath,
6239
6617
  claudeDir,
6240
6618
  recordedHarnessHook(projectDir, logAtStart)
6241
6619
  );
6242
- if (report.staleHookRefs.length > 0) dropHealedHookPortions(projectDir, report.staleHookRefs);
6243
6620
  }
6244
6621
  if (wants("hooks")) report.mcpAllowlistRetired = retireMcpAllowlist(projectDir);
6245
6622
  report.retiredAgents = staleAgentFiles(claudeDir);
@@ -6249,6 +6626,7 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
6249
6626
  const external = wants("external") ? refreshExternalCli(projectDir, harnessRoot, legacyDroppedKeys(logAtStart), harnessMcp) : {
6250
6627
  externalUpdated: 0,
6251
6628
  externalBackedUp: [],
6629
+ displaced: [],
6252
6630
  externalForeignOwned: [],
6253
6631
  externalOutside: [],
6254
6632
  written: [],
@@ -6340,9 +6718,15 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
6340
6718
  report.externalSkillsUnknown = skillRefresh.unknown;
6341
6719
  const outsideLinks = mergeOutside(outside.list(), sharedWrites.outside, external.externalOutside);
6342
6720
  if (outsideLinks.length > 0) report.outsideLinks = outsideLinks;
6721
+ const displacedAll = [...claudeDisplaced, ...external.displaced];
6722
+ const displacedMoved = displacedAll.filter((f) => f.notes.length > 0).map((f) => f.path);
6723
+ const displacedSame = displacedAll.filter((f) => f.notes.length === 0).map((f) => f.path);
6724
+ if (displacedMoved.length > 0) report.displaced = displacedMoved;
6725
+ if (displacedSame.length > 0) report.displacedSame = displacedSame;
6343
6726
  report.backups = collectRunBackups(projectDir, startedAt, backupsAtStart);
6344
6727
  const onRecord = checksumOnRecord(logAtStart);
6345
- const noChecksum = report.backups.map((b2) => b2.path).filter((p2) => !onRecord(p2));
6728
+ const firstContact = new Set(displacedMoved);
6729
+ const noChecksum = report.backups.map((b2) => b2.path).filter((p2) => !onRecord(p2) && !firstContact.has(p2));
6346
6730
  if (noChecksum.length > 0) report.noChecksum = noChecksum;
6347
6731
  writeBackupList(projectDir, report.backups, new Set(noChecksum));
6348
6732
  return report;
@@ -6422,7 +6806,8 @@ function checksumOnRecord(log) {
6422
6806
  const paths = /* @__PURE__ */ new Set([
6423
6807
  ...(log.policyFiles ?? []).map((f) => `.claude/${f.path}`),
6424
6808
  ...(log.skillFiles ?? []).map((f) => `.claude/skills/${f.path}`),
6425
- ...(log.externalFiles ?? []).map((f) => f.path)
6809
+ ...(log.externalFiles ?? []).map((f) => f.path),
6810
+ ...log.assets.flatMap((a) => a.files ?? []).map((f) => f.path)
6426
6811
  ]);
6427
6812
  const anchor = log.templates.rootClaudeMd;
6428
6813
  if (anchor !== void 0) paths.add(anchor.path);
@@ -6435,7 +6820,7 @@ function retiredFeaturesPart(projectDir, f) {
6435
6820
  }
6436
6821
  function legacyHookOnDisk(projectDir, rel) {
6437
6822
  try {
6438
- return hasLegacyHarnessHook(readFileSync21(join28(projectDir, rel), "utf8"));
6823
+ return hasLegacyHarnessHook(readFileSync21(join29(projectDir, rel), "utf8"));
6439
6824
  } catch {
6440
6825
  return false;
6441
6826
  }
@@ -6461,8 +6846,8 @@ function writeUpdateSharedFiles(args) {
6461
6846
  excluded: adapterExcluded(harnessRoot, excluded)
6462
6847
  });
6463
6848
  const tracks = installedTracks(projectDir);
6464
- const settingsSource = join28(templatesDir, "settings.json");
6465
- if (args.claudeManaged && wants("hooks") && existsSync20(settingsSource)) {
6849
+ const settingsSource = join29(templatesDir, "settings.json");
6850
+ if (args.claudeManaged && wants("hooks") && existsSync21(settingsSource)) {
6466
6851
  const spec = buildAssetSpec({ tracks, options: DEFAULT_OPTIONS });
6467
6852
  const manifest = buildManifest(spec);
6468
6853
  writeSettingsShared(
@@ -6472,7 +6857,7 @@ function writeUpdateSharedFiles(args) {
6472
6857
  log,
6473
6858
  (script) => {
6474
6859
  const target = `.claude/hooks/${script}`;
6475
- return manifest.some((e2) => e2.target === target && e2.applies(spec)) && !isBaselineExcluded(target, excluded) && existsSync20(join28(projectDir, target));
6860
+ return manifest.some((e2) => e2.target === target && e2.applies(spec)) && !isBaselineExcluded(target, excluded) && existsSync21(join29(projectDir, target));
6476
6861
  }
6477
6862
  // 기록이 claude 를 깔린 CLI 로 말한다 = install 이 이 파일을 썼다 — 없으면 만든다(훅 배선이 여기 산다)
6478
6863
  );
@@ -6536,11 +6921,11 @@ function catalogRemovedOnRecord(log) {
6536
6921
  }
6537
6922
  function staleSkillDirs(projectDir, claudeDir, clis) {
6538
6923
  const stale = (id) => RENAMED_SKILL_IDS[id] !== void 0 || RETIRED_SKILL_IDS.includes(id);
6539
- const dirs = [join28(claudeDir, "skills")];
6540
- if (clis.some((cli) => cli !== "claude")) dirs.push(join28(projectDir, ".agents", "skills"));
6924
+ const dirs = [join29(claudeDir, "skills")];
6925
+ if (clis.some((cli) => cli !== "claude")) dirs.push(join29(projectDir, ".agents", "skills"));
6541
6926
  const ids = /* @__PURE__ */ new Set();
6542
6927
  for (const dir of dirs) {
6543
- if (!existsSync20(dir)) continue;
6928
+ if (!existsSync21(dir)) continue;
6544
6929
  for (const e2 of readdirSync3(dir, { withFileTypes: true })) {
6545
6930
  if (e2.isDirectory() && stale(e2.name)) ids.add(e2.name);
6546
6931
  }
@@ -6548,14 +6933,14 @@ function staleSkillDirs(projectDir, claudeDir, clis) {
6548
6933
  return [...ids];
6549
6934
  }
6550
6935
  function staleAgentFiles(claudeDir) {
6551
- const dir = join28(claudeDir, "agents");
6552
- if (!existsSync20(dir)) return [];
6936
+ const dir = join29(claudeDir, "agents");
6937
+ if (!existsSync21(dir)) return [];
6553
6938
  return readdirSync3(dir, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".md")).map((e2) => basename3(e2.name, ".md")).filter((id) => RETIRED_AGENT_IDS.includes(id));
6554
6939
  }
6555
6940
  function demotedAgentFiles(claudeDir, tracks) {
6556
6941
  if (tracks.length === 0) return [];
6557
- const dir = join28(claudeDir, "agents");
6558
- if (!existsSync20(dir)) return [];
6942
+ const dir = join29(claudeDir, "agents");
6943
+ if (!existsSync21(dir)) return [];
6559
6944
  const demoted = TRACK_AGENTS.filter(([, pattern]) => !anyTrack(tracks, pattern)).map(
6560
6945
  ([id]) => id
6561
6946
  );
@@ -6577,21 +6962,21 @@ function installNewAssets(projectDir, templatesDir, wants, outside) {
6577
6962
  if (!wants(group)) continue;
6578
6963
  if (entry.target === HARNESS_ANCHOR_FILE) continue;
6579
6964
  if (isBaselineExcluded(entry.target, baselineExcluded)) continue;
6580
- const target = join28(projectDir, entry.target);
6965
+ const target = join29(projectDir, entry.target);
6581
6966
  const foreign = foreignOwnedTarget(projectDir, entry.target);
6582
6967
  if (foreign !== null) {
6583
6968
  if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
6584
6969
  continue;
6585
6970
  }
6586
- if (existsSync20(target)) continue;
6587
- const source = join28(templatesDir, entry.source);
6588
- if (!existsSync20(source)) continue;
6971
+ if (existsSync21(target)) continue;
6972
+ const source = join29(templatesDir, entry.source);
6973
+ if (!existsSync21(source)) continue;
6589
6974
  if (outside.skip(target)) continue;
6590
6975
  if (entry.target.startsWith(".claude/")) {
6591
6976
  if (!claudeSelected) continue;
6592
6977
  if (entry.target === SETTINGS_TARGET) continue;
6593
6978
  }
6594
- mkdirSync5(dirname5(target), { recursive: true });
6979
+ mkdirSync6(dirname6(target), { recursive: true });
6595
6980
  copyFileSync(source, target);
6596
6981
  const recordedAs = entry.target.startsWith(".claude/") ? entry.target.slice(8) : entry.target;
6597
6982
  const wired = entry.target.startsWith(".claude/hooks/") && wiredBefore.has(entry.target.slice(".claude/hooks/".length));
@@ -6611,27 +6996,27 @@ function collectRunBackups(projectDir, startedAt, exclude) {
6611
6996
  const consider = (rel) => {
6612
6997
  if (!BACKUP_SUFFIX.test(rel)) return;
6613
6998
  if (exclude?.has(rel)) return;
6614
- const abs = join28(projectDir, rel);
6615
- if (statSync3(abs).mtimeMs < startedAt - 1e3) return;
6999
+ const abs = join29(projectDir, rel);
7000
+ if (statSync4(abs).mtimeMs < startedAt - 1e3) return;
6616
7001
  out.push({ path: rel.replace(BACKUP_SUFFIX, ""), backup: rel });
6617
7002
  };
6618
7003
  for (const e2 of readdirSync3(projectDir, { withFileTypes: true }))
6619
7004
  if (e2.isFile()) consider(e2.name);
6620
7005
  for (const dir of BACKUP_SCAN_DIRS) {
6621
- for (const rel of listFilesRecursive(join28(projectDir, dir))) consider(`${dir}/${rel}`);
7006
+ for (const rel of listFilesRecursive(join29(projectDir, dir))) consider(`${dir}/${rel}`);
6622
7007
  }
6623
7008
  return out.sort((a, b2) => a.path.localeCompare(b2.path));
6624
7009
  }
6625
7010
  function writeBackupList(projectDir, backups, noChecksum = /* @__PURE__ */ new Set()) {
6626
- const dir = join28(projectDir, ".uzys-agent-harness");
6627
- const file = join28(dir, "update-backups.json");
7011
+ const dir = join29(projectDir, ".uzys-agent-harness");
7012
+ const file = join29(dir, "update-backups.json");
6628
7013
  try {
6629
7014
  if (backups.length === 0) {
6630
- if (existsSync20(file)) unlinkSync2(file);
7015
+ if (existsSync21(file)) unlinkSync2(file);
6631
7016
  return;
6632
7017
  }
6633
- if (!existsSync20(dir)) return;
6634
- writeFileSync7(
7018
+ if (!existsSync21(dir)) return;
7019
+ writeFileSync8(
6635
7020
  file,
6636
7021
  `${JSON.stringify(
6637
7022
  {
@@ -6662,15 +7047,15 @@ function installNewSkillDirs(projectDir, templatesDir, tracks, outside) {
6662
7047
  if (entry.type !== "dir" || !entry.target.startsWith(".claude/skills/")) continue;
6663
7048
  if (!entry.applies(spec) || isBaselineExcluded(entry.target, excluded)) continue;
6664
7049
  if (skillExcluded.has(entry.target.slice(".claude/skills/".length))) continue;
6665
- const target = join28(projectDir, entry.target);
7050
+ const target = join29(projectDir, entry.target);
6666
7051
  const foreign = foreignOwnedTarget(projectDir, entry.target);
6667
7052
  if (foreign !== null) {
6668
7053
  if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
6669
7054
  continue;
6670
7055
  }
6671
- if (existsSync20(target)) continue;
6672
- const source = join28(templatesDir, entry.source);
6673
- if (!existsSync20(source)) continue;
7056
+ if (existsSync21(target)) continue;
7057
+ const source = join29(templatesDir, entry.source);
7058
+ if (!existsSync21(source)) continue;
6674
7059
  if (outside.skip(target)) continue;
6675
7060
  copyDir(source, target, (rel) => foreignOwnedTarget(projectDir, `${entry.target}/${rel}`));
6676
7061
  if (priorIds.has(entry.target.slice(".claude/skills/".length))) restored.push(entry.target);
@@ -6681,6 +7066,7 @@ function installNewSkillDirs(projectDir, templatesDir, tracks, outside) {
6681
7066
  function recordNewSkillBaseline(projectDir, templatesDir, targets) {
6682
7067
  const log = readInstallLog(projectDir);
6683
7068
  if (!log) return;
7069
+ if (log.skillFiles === void 0) return;
6684
7070
  const ids = new Set(targets.map((t) => t.slice(".claude/skills/".length)));
6685
7071
  const added = collectSkillHashes(projectDir, templatesDir).filter(
6686
7072
  (f) => ids.has(f.path.split("/")[0] ?? "")
@@ -6700,14 +7086,14 @@ function trackOnlyFileAssets(tracks) {
6700
7086
  );
6701
7087
  }
6702
7088
  function syncHarnessAnchor(projectDir, templatesDir, report, outside, now = /* @__PURE__ */ new Date()) {
6703
- if (existsSync20(join28(projectDir, LEGACY_ANCHOR_FILE2))) {
7089
+ if (existsSync21(join29(projectDir, LEGACY_ANCHOR_FILE2))) {
6704
7090
  report.legacyAnchor = LEGACY_ANCHOR_FILE2;
6705
7091
  }
6706
- const templateMd = join28(templatesDir, "CLAUDE.md");
6707
- if (!existsSync20(templateMd)) return;
6708
- const anchor = join28(projectDir, HARNESS_ANCHOR_FILE);
7092
+ const templateMd = join29(templatesDir, "CLAUDE.md");
7093
+ if (!existsSync21(templateMd)) return;
7094
+ const anchor = join29(projectDir, HARNESS_ANCHOR_FILE);
6709
7095
  if (outside.skip(anchor)) return;
6710
- const existed = existsSync20(anchor);
7096
+ const existed = existsSync21(anchor);
6711
7097
  if (existed) {
6712
7098
  const current = readFileSync21(anchor, "utf8");
6713
7099
  if (current !== readFileSync21(templateMd, "utf8")) {
@@ -6731,21 +7117,21 @@ function syncHarnessAnchor(projectDir, templatesDir, report, outside, now = /* @
6731
7117
  recordAnchorBaseline(projectDir, anchor);
6732
7118
  }
6733
7119
  function upsertRootImport(projectDir, outside) {
6734
- const target = join28(projectDir, "CLAUDE.md");
7120
+ const target = join29(projectDir, "CLAUDE.md");
6735
7121
  if (outside.skip(target)) return false;
6736
- const existing = existsSync20(target) ? readFileSync21(target, "utf8") : null;
7122
+ const existing = existsSync21(target) ? readFileSync21(target, "utf8") : null;
6737
7123
  const next = upsertHarnessImport(existing, {
6738
7124
  projectName: basename3(projectDir),
6739
7125
  tracks: installedTracks(projectDir),
6740
7126
  // ADR-085 — update 는 선택 목록을 인자로 받지 않는다. `syncSkills` 와 같은 판정을 쓴다:
6741
7127
  // `.claude/skills/<id>` 가 있으면 그 스킬은 이 설치가 고른 것이다.
6742
7128
  continuousSkills: CONTINUOUS_SKILLS.map((s) => s.id).filter(
6743
- (id) => existsSync20(join28(projectDir, ".claude/skills", id))
7129
+ (id) => existsSync21(join29(projectDir, ".claude/skills", id))
6744
7130
  )
6745
7131
  });
6746
7132
  if (next === existing) return false;
6747
7133
  if (existing !== null) backupIfLossyUtf8(target);
6748
- writeFileSync7(target, next);
7134
+ writeFileSync8(target, next);
6749
7135
  return true;
6750
7136
  }
6751
7137
  function installedTracks(projectDir) {
@@ -6775,7 +7161,7 @@ function excludedBaselineOnDisk(projectDir, log) {
6775
7161
  if (excluded.size === 0) return [];
6776
7162
  const spec = buildAssetSpec({ tracks: installedTracks(projectDir), options: DEFAULT_OPTIONS });
6777
7163
  return buildManifest(spec).filter(
6778
- (e2) => e2.applies(spec) && isBaselineExcluded(e2.target, excluded) && existsSync20(join28(projectDir, e2.target))
7164
+ (e2) => e2.applies(spec) && isBaselineExcluded(e2.target, excluded) && existsSync21(join29(projectDir, e2.target))
6779
7165
  ).map((e2) => e2.target);
6780
7166
  }
6781
7167
  function excludedSkillOf(log) {
@@ -6785,12 +7171,12 @@ function excludedSkillOf(log) {
6785
7171
  function recordedRenderInputs(projectDir, log) {
6786
7172
  const excluded = excludedIds(log);
6787
7173
  return {
6788
- // 스킬은 **디스크에 있는 것만** 넘긴다. 스킬 *파일*은 전체 목록을 넘겨도 refreshOnly 가
6789
- // 없는 것을 건너뛰지만, `AGENTS.md` 의 상시 스킬 안내(ADR-085)는 이 목록 그대로 렌더돼
6790
- // `--without` 으로 뺀 스킬(#505)·opt-in 스킬을 "열어라"고 적었다(실측 2026-09-21). 판정은
6791
- // `upsertRootImport` 와 같다 — `.claude/skills/<id>` 또는 `.agents/skills/<id>` 가 있으면 깔린 것.
7174
+ // 스킬은 **기록 + 이번 릴리즈 기본**만 넘긴다(#761 — `recordedBundledSkills`). 스킬 *파일*은 전체 목록을 넘겨도
7175
+ // refreshOnly 가 없는 것을 건너뛰지만, `AGENTS.md` 의 상시 스킬 안내(ADR-085)는 이 목록 그대로 렌더돼
7176
+ // `--without` 으로 뺀 스킬(#505)·opt-in 스킬을 "열어라"고 적었다(실측 2026-09-21). 디스크에 같은 이름의 폴더가
7177
+ // 있다는 것은 그 스킬을 깔았다는 증거가 아니다(ADR-096 D6) — 손으로 만든 opt-in 이름 폴더를 덮지 않는다.
6792
7178
  // ADR-099 R3 — 뺀 스킬은 디스크에 남아 있어도 갱신하지도 안내하지도 않는다(install 이 내는 것과 같은 목록)
6793
- selectedInternalSkills: installedBundledSkills(projectDir).filter(
7179
+ selectedInternalSkills: recordedBundledSkills(projectDir, log).filter(
6794
7180
  (id) => !excludedSkillOf(log)(id)
6795
7181
  ),
6796
7182
  // #601 — install 과 같은 SSOT(resolveRules)를 설치된 트랙으로. 렌더 경로(AGENTS.md 절 · antigravity 룰 생성)는
@@ -6800,11 +7186,29 @@ function recordedRenderInputs(projectDir, log) {
6800
7186
  )
6801
7187
  };
6802
7188
  }
6803
- function installedBundledSkills(projectDir) {
6804
- return INTERNAL_BUNDLED_SKILL_IDS.filter(
6805
- (id) => existsSync20(join28(projectDir, ".claude/skills", id)) || existsSync20(join28(projectDir, ".agents/skills", id))
6806
- );
7189
+ function recordedBundledSkills(projectDir, log) {
7190
+ if (log === null) {
7191
+ return INTERNAL_BUNDLED_SKILL_IDS.filter(
7192
+ (id) => existsSync21(join29(projectDir, ".claude/skills", id)) || existsSync21(join29(projectDir, ".agents/skills", id))
7193
+ );
7194
+ }
7195
+ const ids = recordedSkillIds(projectDir, log);
7196
+ return INTERNAL_BUNDLED_SKILL_IDS.filter((id) => ids.has(id));
6807
7197
  }
7198
+ function recordedSkillIds(projectDir, log) {
7199
+ const isTarget = recordedTrackTargets(log);
7200
+ const ids = new Set(bundledSkillTargets(projectDir, log));
7201
+ for (const f of log.skillFiles ?? []) {
7202
+ const id = f.path.split("/")[0] ?? "";
7203
+ if (recorded(log, `.claude/skills/${f.path}`, { isTarget }).state !== "none") ids.add(id);
7204
+ }
7205
+ for (const f of log.externalFiles ?? []) {
7206
+ const id = SHARED_SKILL_ID.exec(f.path)?.[1];
7207
+ if (id !== void 0) ids.add(id);
7208
+ }
7209
+ return ids;
7210
+ }
7211
+ var SHARED_SKILL_ID = /^\.agents\/skills\/([^/]+)\//;
6808
7212
  function installedCliTargets(log) {
6809
7213
  if (log === null) return ALL_CLI_TARGETS;
6810
7214
  return installedClis(log);
@@ -6832,9 +7236,9 @@ var SHARED_SKILLS_DIR = ".agents/skills";
6832
7236
  var SHARED_SKILL_FILE = /^\.agents\/skills\/([^/]+)\//;
6833
7237
  var SHARED_SKILL_MD = /^\.agents\/skills\/([^/]+)\/SKILL\.md$/;
6834
7238
  function sharedSkillIdsOnDisk(projectDir) {
6835
- const root = join28(projectDir, SHARED_SKILLS_DIR);
6836
- if (!existsSync20(root)) return /* @__PURE__ */ new Set();
6837
- return new Set(readdirSync3(root).filter((id) => existsSync20(join28(root, id, "SKILL.md"))));
7239
+ const root = join29(projectDir, SHARED_SKILLS_DIR);
7240
+ if (!existsSync21(root)) return /* @__PURE__ */ new Set();
7241
+ return new Set(readdirSync3(root).filter((id) => existsSync21(join29(root, id, "SKILL.md"))));
6838
7242
  }
6839
7243
  var CREATED_SHARED_HOW = {
6840
7244
  "opencode.json": "created from template (harness part + template defaults)",
@@ -6861,14 +7265,14 @@ function refreshExternalCli(projectDir, harnessRoot, droppedKeys, harnessMcp) {
6861
7265
  const log = readInstallLog(projectDir);
6862
7266
  const presentBefore = sharedSkillIdsOnDisk(projectDir);
6863
7267
  const rulesBefore = /* @__PURE__ */ new Set();
6864
- const rulesDir = join28(projectDir, ".agents", "rules");
6865
- if (existsSync20(rulesDir)) {
7268
+ const rulesDir = join29(projectDir, ".agents", "rules");
7269
+ if (existsSync21(rulesDir)) {
6866
7270
  try {
6867
7271
  for (const f of readdirSync3(rulesDir)) rulesBefore.add(f);
6868
7272
  } catch {
6869
7273
  }
6870
7274
  }
6871
- const agentsMdBefore = existsSync20(join28(projectDir, "AGENTS.md"));
7275
+ const agentsMdBefore = existsSync21(join29(projectDir, "AGENTS.md"));
6872
7276
  const result = runCliTransforms({
6873
7277
  harnessRoot,
6874
7278
  projectDir,
@@ -6884,7 +7288,10 @@ function refreshExternalCli(projectDir, harnessRoot, droppedKeys, harnessMcp) {
6884
7288
  shared: { portions: log?.portions ?? [], excluded: [...excludedIds(log)] },
6885
7289
  // ADR-099 R2 (#584) — 기록이 깔린 CLI 로 말하면 그 CLI 의 앵커·룰을 없어도 만든다(디스크의 앵커 존재가 아니라 기록)
6886
7290
  // 기록 없음: CLI 경로로는 도달 불가 — 진입 판정(#699)이 먼저 거절. 엔진 단위 방어(빈 집합).
6887
- recordedClis: log === null ? [] : installedClis(log)
7291
+ recordedClis: log === null ? [] : installedClis(log),
7292
+ // #761 B-1 — 외부 기록이 아예 없던 옛 판의 산출물은 첫 접촉이 아니라 체크섬 없는 하네스 파일이다(install 과 같은 판정).
7293
+ // 앞 단계(`refreshNeutralHelpers`)가 헬퍼만 든 `externalFiles` 를 적어도 같은 답이다 — 헬퍼는 외부 자리로 세지 않는다(B-2)
7294
+ owned: recordedOwner(log)
6888
7295
  });
6889
7296
  if (log) {
6890
7297
  const retiredPath = result.anchorRetired?.forget === true ? result.anchorRetired.path : null;
@@ -6902,7 +7309,8 @@ function refreshExternalCli(projectDir, harnessRoot, droppedKeys, harnessMcp) {
6902
7309
  if (portions.length > 0) next.portions = portions;
6903
7310
  else delete next.portions;
6904
7311
  const created = result.sharedFiles.filter((f) => f.createdAsRoot === true).map((f) => ({ path: f.path, change: "created", notes: ["\uD558\uB124\uC2A4 \uBAAB\uB9CC(\uBE14\uB85D)"] }));
6905
- if (created.length > 0) next.rootFiles = mergeRootFiles(log.rootFiles, created);
7312
+ const roots = [...created, ...withoutLostBackups(log.rootFiles, result.displaced ?? [])];
7313
+ if (roots.length > 0) next.rootFiles = mergeRootFiles(log.rootFiles, roots);
6906
7314
  try {
6907
7315
  writeInstallLog(projectDir, next);
6908
7316
  } catch {
@@ -6959,6 +7367,7 @@ function refreshExternalCli(projectDir, harnessRoot, droppedKeys, harnessMcp) {
6959
7367
  return {
6960
7368
  externalUpdated: result.externalUpdated,
6961
7369
  externalBackedUp: result.externalBackedUp,
7370
+ displaced: result.displaced ?? [],
6962
7371
  externalForeignOwned: result.externalForeignOwned,
6963
7372
  externalOutside: result.externalOutside,
6964
7373
  written: result.externalFiles.map((f) => f.path),
@@ -7001,92 +7410,154 @@ function refreshExternalCli(projectDir, harnessRoot, droppedKeys, harnessMcp) {
7001
7410
  })
7002
7411
  };
7003
7412
  }
7004
- function isHarnessOwned2(ctx, rel, current) {
7005
- return isHarnessOwned(ctx.baseline, `${ctx.prefix}/${rel}`, current);
7006
- }
7007
7413
  function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
7008
- if (!existsSync20(target) || !existsSync20(source)) return { updated: 0, backedUp: [] };
7414
+ if (!existsSync21(target) || !existsSync21(source))
7415
+ return { updated: 0, backedUp: [], displaced: [] };
7009
7416
  let updated = 0;
7010
7417
  const backedUp = [];
7418
+ const displaced = [];
7011
7419
  for (const file of readdirSync3(target)) {
7012
7420
  if (!file.endsWith(ext)) continue;
7013
- const targetFile = join28(target, file);
7014
- const sourceFile = join28(source, file);
7015
- if (!existsSync20(sourceFile)) continue;
7421
+ const targetFile = join29(target, file);
7422
+ const sourceFile = join29(source, file);
7423
+ if (!existsSync21(sourceFile)) continue;
7016
7424
  if (ctx.outside?.skip(targetFile)) continue;
7017
- if (ctx.excluded && isBaselineExcluded(`.claude/${ctx.prefix}/${file}`, ctx.excluded)) continue;
7425
+ const projectRel = `.claude/${ctx.prefix}/${file}`;
7426
+ if (ctx.excluded && isBaselineExcluded(projectRel, ctx.excluded)) continue;
7427
+ if (ctx.justWritten?.has(projectRel)) continue;
7018
7428
  const next = readFileSync21(sourceFile, "utf8");
7019
- const current = readFileSync21(targetFile, "utf8");
7020
- if (current === next) continue;
7021
- if (!isHarnessOwned2(ctx, file, current)) {
7022
- backupFile(targetFile, now);
7023
- backedUp.push(`${ctx.prefix}/${file}`);
7024
- }
7025
- writeFileSync7(targetFile, next);
7026
- updated++;
7429
+ const rec = ctx.recordOf?.(projectRel) ?? baselineRecord(ctx.baseline, `${ctx.prefix}/${file}`);
7430
+ const w3 = judgedUpdateWrite(targetFile, rec, next, now);
7431
+ if (w3.changed) updated++;
7432
+ if (w3.kind === "edited") backedUp.push(`${ctx.prefix}/${file}`);
7433
+ const d2 = displacedEntry(w3, projectRel, targetFile);
7434
+ if (d2 !== null) displaced.push(d2);
7027
7435
  }
7028
- return { updated, backedUp };
7436
+ return { updated, backedUp, displaced };
7029
7437
  }
7030
- function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Date(), foreignOf, outside, excludedSkill) {
7031
- if (!existsSync20(targetDir) || !existsSync20(sourceDir))
7032
- return { updated: 0, backedUp: [], skippedLinks: [], foreignOwned: [], pruned: [] };
7438
+ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Date(), foreignOf, outside, excludedSkill, opts = {}) {
7439
+ if (!existsSync21(targetDir) || !existsSync21(sourceDir))
7440
+ return {
7441
+ updated: 0,
7442
+ backedUp: [],
7443
+ skippedLinks: [],
7444
+ foreignOwned: [],
7445
+ pruned: [],
7446
+ displaced: []
7447
+ };
7033
7448
  let updated = 0;
7034
7449
  const backedUp = [];
7035
7450
  const skippedLinks = [];
7036
7451
  const foreignOwned = [];
7037
7452
  const pruned = [];
7453
+ const displaced = [];
7038
7454
  for (const skill of readdirSync3(sourceDir, { withFileTypes: true })) {
7039
7455
  if (!skill.isDirectory()) continue;
7040
- const targetSkill = join28(targetDir, skill.name);
7456
+ const targetSkill = join29(targetDir, skill.name);
7041
7457
  if (occupiedByNonDirectory(targetSkill)) {
7042
7458
  skippedLinks.push(skill.name);
7043
7459
  continue;
7044
7460
  }
7045
- if (!existsSync20(targetSkill)) continue;
7461
+ if (opts.targets !== void 0 && !opts.targets.has(skill.name)) continue;
7462
+ if (!existsSync21(targetSkill)) continue;
7046
7463
  if (excludedSkill?.(skill.name)) continue;
7047
- for (const rel of listFilesRecursive(join28(sourceDir, skill.name))) {
7048
- const targetFile = join28(targetSkill, rel);
7464
+ for (const rel of listFilesRecursive(join29(sourceDir, skill.name))) {
7465
+ const targetFile = join29(targetSkill, rel);
7049
7466
  const foreign = foreignOf(`${skill.name}/${rel}`);
7050
7467
  if (foreign !== null) {
7051
7468
  if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
7052
7469
  continue;
7053
7470
  }
7054
7471
  if (outside?.skip(targetFile)) continue;
7055
- const next = readFileSync21(join28(sourceDir, skill.name, rel), "utf8");
7056
- if (!existsSync20(targetFile)) {
7057
- mkdirSync5(dirname5(targetFile), { recursive: true });
7058
- writeFileSync7(targetFile, next);
7059
- updated++;
7060
- continue;
7061
- }
7062
- const current = readFileSync21(targetFile, "utf8");
7063
- if (current === next) continue;
7064
- const recorded2 = baseline.get(`${skill.name}/${rel}`);
7065
- if (recorded2 === void 0 || hashContent(current) !== recorded2) {
7066
- backupFile(targetFile, now);
7067
- backedUp.push(`${skill.name}/${rel}`);
7068
- }
7069
- writeFileSync7(targetFile, next);
7070
- updated++;
7071
- }
7072
- const bundled = new Set(listFilesRecursive(join28(sourceDir, skill.name)));
7472
+ const next = readFileSync21(join29(sourceDir, skill.name, rel), "utf8");
7473
+ const projectRel = `.claude/skills/${skill.name}/${rel}`;
7474
+ const rec = opts.recordOf?.(projectRel) ?? baselineRecord(baseline, `${skill.name}/${rel}`);
7475
+ const w3 = judgedUpdateWrite(targetFile, rec, next, now);
7476
+ if (w3.changed) updated++;
7477
+ if (w3.kind === "edited") backedUp.push(`${skill.name}/${rel}`);
7478
+ const d2 = displacedEntry(w3, projectRel, targetFile);
7479
+ if (d2 !== null) displaced.push(d2);
7480
+ }
7481
+ const bundled = new Set(listFilesRecursive(join29(sourceDir, skill.name)));
7073
7482
  for (const rel of listFilesRecursive(targetSkill)) {
7074
7483
  if (bundled.has(rel) || basename3(rel).includes(".backup-")) continue;
7075
7484
  if (foreignOf(`${skill.name}/${rel}`) !== null) continue;
7076
- const targetFile = join28(targetSkill, rel);
7485
+ const targetFile = join29(targetSkill, rel);
7077
7486
  if (outside?.skip(targetFile)) continue;
7078
- const recorded2 = baseline.get(`${skill.name}/${rel}`);
7079
- if (recorded2 === void 0 || hashContent(readFileSync21(targetFile, "utf8")) !== recorded2) {
7487
+ const rec = opts.recordOf?.(`.claude/skills/${skill.name}/${rel}`) ?? baselineRecord(baseline, `${skill.name}/${rel}`);
7488
+ if (rec.state !== "sha" || hashContent(readFileSync21(targetFile, "utf8")) !== rec.sha256) {
7080
7489
  backupFile(targetFile, now);
7081
7490
  backedUp.push(`${skill.name}/${rel}`);
7082
7491
  }
7083
7492
  unlinkSync2(targetFile);
7084
7493
  pruned.push(`${skill.name}/${rel}`);
7085
- for (let d2 = dirname5(targetFile); d2 !== targetSkill && readdirSync3(d2).length === 0; d2 = dirname5(d2))
7494
+ for (let d2 = dirname6(targetFile); d2 !== targetSkill && readdirSync3(d2).length === 0; d2 = dirname6(d2))
7086
7495
  rmdirSync(d2);
7087
7496
  }
7088
7497
  }
7089
- return { updated, backedUp, skippedLinks, foreignOwned, pruned };
7498
+ return { updated, backedUp, skippedLinks, foreignOwned, pruned, displaced };
7499
+ }
7500
+ function judgedUpdateWrite(abs, rec, next, now) {
7501
+ const disk = existsSync21(abs) ? readFileSync21(abs, "utf8") : null;
7502
+ const j4 = judge({ op: "write", kind: "harness", rec, disk, next, run: "update" });
7503
+ let backup;
7504
+ switch (j4.verdict) {
7505
+ case "leave":
7506
+ break;
7507
+ case "create":
7508
+ mkdirSync6(dirname6(abs), { recursive: true });
7509
+ writeFileSync8(abs, next);
7510
+ break;
7511
+ case "overwrite":
7512
+ writeFileSync8(abs, next);
7513
+ break;
7514
+ case "backup+overwrite":
7515
+ backup = backupFile(abs, now);
7516
+ writeFileSync8(abs, next);
7517
+ break;
7518
+ default:
7519
+ throw new Error(`update: '${j4.verdict}' is not a write verdict (${abs})`);
7520
+ }
7521
+ const changed = j4.verdict !== "leave";
7522
+ if (j4.record === "displaced") return { kind: "displacedSame", changed };
7523
+ if (backup === void 0) return { kind: changed ? "written" : "same", changed };
7524
+ return { kind: j4.record === "displaced+sha" ? "displaced" : "edited", changed, backup };
7525
+ }
7526
+ function displacedEntry(w3, projectRel, abs) {
7527
+ if (w3.kind === "displacedSame") return { path: projectRel, change: "displaced", notes: [] };
7528
+ if (w3.kind !== "displaced" || w3.backup === void 0) return null;
7529
+ const backupRel = `${projectRel}${w3.backup.slice(abs.length)}`;
7530
+ return { path: projectRel, change: "displaced", notes: [backupRel] };
7531
+ }
7532
+ function baselineRecord(baseline, key) {
7533
+ const sha256 = baseline.get(key);
7534
+ return sha256 === void 0 ? { state: "no-sha" } : { state: "sha", sha256 };
7535
+ }
7536
+ function recordedStateOf(log) {
7537
+ if (log === null) return void 0;
7538
+ const isTarget = recordedTrackTargets(log);
7539
+ return (projectRel) => recorded(log, projectRel, { isTarget });
7540
+ }
7541
+ function notOursUnder(displaced, prefix) {
7542
+ return new Set(
7543
+ displaced.filter((f) => f.notes.length === 0 && f.path.startsWith(prefix)).map((f) => f.path.slice(prefix.length))
7544
+ );
7545
+ }
7546
+ function withoutLostBackups(prior, add) {
7547
+ const backedUp = new Set(
7548
+ (prior ?? []).filter((f) => f.change === "displaced" && f.notes.length > 0).map((f) => f.path)
7549
+ );
7550
+ return add.filter((f) => f.notes.length > 0 || !backedUp.has(f.path));
7551
+ }
7552
+ function recordDisplaced(projectDir, displaced) {
7553
+ const log = readInstallLog(projectDir);
7554
+ if (!log) return;
7555
+ const add = withoutLostBackups(log.rootFiles, displaced);
7556
+ if (add.length === 0) return;
7557
+ try {
7558
+ writeInstallLog(projectDir, { ...log, rootFiles: mergeRootFiles(log.rootFiles, add) });
7559
+ } catch {
7560
+ }
7090
7561
  }
7091
7562
  function skillBaseline(projectDir) {
7092
7563
  const log = readInstallLog(projectDir);
@@ -7096,15 +7567,18 @@ function policyBaseline(projectDir) {
7096
7567
  const log = readInstallLog(projectDir);
7097
7568
  return new Map((log?.policyFiles ?? []).map((f) => [f.path, f.sha256]));
7098
7569
  }
7099
- function refreshPolicyBaseline(projectDir, templatesDir, dirs, outside) {
7570
+ function refreshPolicyBaseline(projectDir, templatesDir, dirs, outside, notOurs = /* @__PURE__ */ new Set()) {
7100
7571
  const log = readInstallLog(projectDir);
7101
7572
  if (!log) return;
7102
7573
  const excluded = excludedIds(log);
7103
7574
  const inDirs = (path) => (dirs === void 0 || dirs.some((d2) => path.startsWith(`${d2}/`))) && !outside.has(path) && !isBaselineExcluded(`.claude/${path}`, excluded);
7104
- const policyFiles = [
7575
+ const rewritten = [
7105
7576
  ...(log.policyFiles ?? []).filter((f) => !inDirs(f.path)),
7106
- ...collectPolicyHashes(projectDir, templatesDir).filter((f) => inDirs(f.path))
7577
+ ...collectPolicyHashes(projectDir, templatesDir).filter(
7578
+ (f) => inDirs(f.path) && !notOurs.has(f.path)
7579
+ )
7107
7580
  ];
7581
+ const policyFiles = [...rewritten, ...retiredClaims(log, projectDir, rewritten)];
7108
7582
  const next = { ...log };
7109
7583
  if (policyFiles.length > 0) next.policyFiles = policyFiles;
7110
7584
  else delete next.policyFiles;
@@ -7118,13 +7592,16 @@ function outsidePaths(outside, prefix) {
7118
7592
  outside.list().filter((o) => o.path.startsWith(prefix)).map((o) => o.path.slice(prefix.length))
7119
7593
  );
7120
7594
  }
7121
- function refreshSkillBaseline(projectDir, templatesDir, outside) {
7595
+ function refreshSkillBaseline(projectDir, templatesDir, outside, targets, notOurs = /* @__PURE__ */ new Set()) {
7122
7596
  const log = readInstallLog(projectDir);
7123
7597
  if (!log) return;
7124
7598
  const excludedSkill = excludedSkillOf(log);
7125
7599
  const keepPrior = (path) => outside.has(path) || excludedSkill(path.split("/")[0] ?? "");
7600
+ const ours = (path) => (targets === void 0 || targets.has(path.split("/")[0] ?? "")) && !notOurs.has(path);
7126
7601
  const skillFiles = [
7127
- ...collectSkillHashes(projectDir, templatesDir).filter((f) => !keepPrior(f.path)),
7602
+ ...collectSkillHashes(projectDir, templatesDir).filter(
7603
+ (f) => !keepPrior(f.path) && ours(f.path)
7604
+ ),
7128
7605
  ...(log.skillFiles ?? []).filter((f) => keepPrior(f.path))
7129
7606
  ];
7130
7607
  const next = { ...log };
@@ -7136,18 +7613,20 @@ function refreshSkillBaseline(projectDir, templatesDir, outside) {
7136
7613
  }
7137
7614
  }
7138
7615
  function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
7139
- if (!existsSync20(target) || !existsSync20(source)) return [];
7616
+ if (!existsSync21(target) || !existsSync21(source)) return [];
7140
7617
  const removed = [];
7141
7618
  for (const file of readdirSync3(target)) {
7142
7619
  if (!file.endsWith(ext)) continue;
7143
- const sourceFile = join28(source, file);
7144
- if (!existsSync20(sourceFile)) {
7145
- const targetFile = join28(target, file);
7146
- if (!ctx.baseline.has(`${ctx.prefix}/${file}`)) continue;
7620
+ const sourceFile = join29(source, file);
7621
+ if (!existsSync21(sourceFile)) {
7622
+ const targetFile = join29(target, file);
7623
+ const key = `${ctx.prefix}/${file}`;
7624
+ if (!ctx.baseline.has(key)) continue;
7147
7625
  if (ctx.outside?.skip(targetFile)) continue;
7148
7626
  try {
7149
7627
  const current = readFileSync21(targetFile, "utf8");
7150
- if (!isHarnessOwned2(ctx, file, current)) backupFile(targetFile, now);
7628
+ const owned = ctx.baseline.get(key) === hashContent(current) && ctx.recordOf?.(`.claude/${key}`).state !== "no-sha";
7629
+ if (!owned) backupFile(targetFile, now);
7151
7630
  unlinkSync2(targetFile);
7152
7631
  removed.push(file);
7153
7632
  } catch {
@@ -7157,8 +7636,8 @@ function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()
7157
7636
  return removed;
7158
7637
  }
7159
7638
  function retireMcpAllowlist(projectDir, now = /* @__PURE__ */ new Date()) {
7160
- const target = join28(projectDir, ".mcp-allowlist");
7161
- if (!existsSync20(target)) return null;
7639
+ const target = join29(projectDir, ".mcp-allowlist");
7640
+ if (!existsSync21(target)) return null;
7162
7641
  try {
7163
7642
  const backup = backupFile(target, now);
7164
7643
  unlinkSync2(target);
@@ -7173,9 +7652,9 @@ function refreshNeutralHelpers(projectDir, templatesDir, report) {
7173
7652
  for (const entry of buildManifest(spec)) {
7174
7653
  if (entry.type !== "file" || !isCliNeutralTarget(entry.target)) continue;
7175
7654
  if (!entry.applies(spec)) continue;
7176
- const abs = join28(projectDir, entry.target);
7177
- const source = join28(templatesDir, entry.source);
7178
- if (!existsSync20(source)) continue;
7655
+ const abs = join29(projectDir, entry.target);
7656
+ const source = join29(templatesDir, entry.source);
7657
+ if (!existsSync21(source)) continue;
7179
7658
  let st2;
7180
7659
  try {
7181
7660
  st2 = lstatSync4(abs);
@@ -7197,7 +7676,7 @@ function refreshNeutralHelpers(projectDir, templatesDir, report) {
7197
7676
  continue;
7198
7677
  }
7199
7678
  if (recorded2 === void 0 || recorded2.sha256 !== diskSha) backupFile(abs);
7200
- writeFileSync7(abs, freshContent, "utf8");
7679
+ writeFileSync8(abs, freshContent, "utf8");
7201
7680
  refreshed.set(entry.target, freshSha);
7202
7681
  report.updated[entry.target] = (report.updated[entry.target] ?? 0) + 1;
7203
7682
  }
@@ -7221,34 +7700,16 @@ function recordClaudeCommand(log, tracks) {
7221
7700
  " "
7222
7701
  );
7223
7702
  }
7224
- function dropHealedHookPortions(projectDir, healedFiles) {
7225
- const healed = new Set(healedFiles.map((f) => f.split("/").pop() ?? f));
7226
- const isHealedKey = (key) => {
7227
- const at5 = key.lastIndexOf("#");
7228
- return at5 !== -1 && healed.has(key.slice(at5 + 1));
7229
- };
7230
- const log = readInstallLog(projectDir);
7231
- if (log === null) return;
7232
- const portions = (log.portions ?? []).filter(
7233
- (p2) => !(p2.path === join28(".claude", "settings.json") && isHealedKey(p2.key))
7234
- );
7235
- if (portions.length > 0) log.portions = portions;
7236
- else delete log.portions;
7237
- try {
7238
- writeInstallLog(projectDir, log);
7239
- } catch {
7240
- }
7241
- }
7242
7703
 
7243
7704
  // src/installer.ts
7244
7705
  function runInstall(ctx) {
7245
7706
  const { harnessRoot, projectDir } = ctx;
7246
7707
  const mode = ctx.mode ?? "fresh";
7247
- const templatesDir = join29(harnessRoot, "templates");
7248
- if (!existsSync21(templatesDir)) {
7708
+ const templatesDir = join30(harnessRoot, "templates");
7709
+ if (!existsSync22(templatesDir)) {
7249
7710
  throw new Error(`Templates dir not found: ${templatesDir}`);
7250
7711
  }
7251
- const claudeDir = join29(projectDir, ".claude");
7712
+ const claudeDir = join30(projectDir, ".claude");
7252
7713
  const previousLog = readInstallLog(projectDir);
7253
7714
  const legacyAnchor = mode === "update" ? null : legacyAnchorTrace(projectDir, ctx.spec);
7254
7715
  if (mode === "update" && previousLog === null) {
@@ -7256,17 +7717,13 @@ function runInstall(ctx) {
7256
7717
  `Update mode requires an install record at ${installLogPath(projectDir)} \u2014 run: agent-harness install --track <name>`
7257
7718
  );
7258
7719
  }
7259
- if (mode === "update" && !existsSync21(claudeDir) && installedClis(previousLog).includes("claude")) {
7720
+ if (mode === "update" && !existsSync22(claudeDir) && installedClis(previousLog).includes("claude")) {
7260
7721
  throw new Error(
7261
7722
  `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>`
7262
7723
  );
7263
7724
  }
7264
7725
  if (mode === "update" && previousLog !== null) {
7265
- return runUpdateInstall(
7266
- ctx,
7267
- templatesDir,
7268
- resolveUpdateBackupPath(ctx, claudeDir, previousLog)
7269
- );
7726
+ return runUpdateInstall(ctx, templatesDir);
7270
7727
  }
7271
7728
  const { spec, excluded } = thisRunExclusions(
7272
7729
  ctx.spec,
@@ -7277,8 +7734,8 @@ function runInstall(ctx) {
7277
7734
  const manifestSpec = buildManifestSpec(spec);
7278
7735
  const baselineExcluded = new Set(spec.baselineExclude ?? []);
7279
7736
  if (spec.cli.includes("claude")) {
7280
- const settingsPath = join29(projectDir, ".claude", "settings.json");
7281
- if (existsSync21(settingsPath)) {
7737
+ const settingsPath = join30(projectDir, ".claude", "settings.json");
7738
+ if (existsSync22(settingsPath)) {
7282
7739
  try {
7283
7740
  JSON.parse(readFileSync22(settingsPath, "utf8"));
7284
7741
  } catch {
@@ -7352,6 +7809,7 @@ function runInstallStages(ctx, args) {
7352
7809
  externalBackups,
7353
7810
  externalUpdated: _externalUpdated,
7354
7811
  externalBackedUp: _externalBackedUp,
7812
+ displaced: cliDisplaced = [],
7355
7813
  externalForeignOwned,
7356
7814
  externalOutside,
7357
7815
  sharedFiles: cliSharedFiles,
@@ -7383,6 +7841,8 @@ function runInstallStages(ctx, args) {
7383
7841
  previousExternal: previousLog?.externalFiles ?? [],
7384
7842
  // #642 (ADR-096 D2) — CLI 집합은 누적이다: 기록된 CLI 도 `AGENTS.md` 집합에 들어간다(`--reinstall` 도 누적)
7385
7843
  recordedClis: previousLog === null ? [] : installedClis(previousLog),
7844
+ // #761 B-1 — 외부 기록이 아예 없던 옛 판의 산출물은 첫 접촉이 아니라 체크섬 없는 하네스 파일이다(`.claude/` B3 와 같은 판정)
7845
+ owned: recordedOwner(previousLog),
7386
7846
  // ADR-097 결정 2 — 범위 조건 없이 `--with-codex-trust` 하나로 정한다.
7387
7847
  codexTrust: spec.options.withCodexTrust,
7388
7848
  // #551 R2 — `.codex/config.toml` · `opencode.json` · 첫 접촉 `AGENTS.md` 의 몫도 `.mcp.json` 과 같은 왕복을
@@ -7433,8 +7893,6 @@ function runInstallStages(ctx, args) {
7433
7893
  filesCopied: base.filesCopied,
7434
7894
  dirsCopied: base.dirsCopied,
7435
7895
  skipped: base.skipped,
7436
- // #551 PR-3 — install · `--reinstall` 은 폴더를 옮기지 않는다. 백업은 파일마다 `backups`/`judged` 에 있다.
7437
- backup: null,
7438
7896
  installedTracks: [...spec.tracks].sort(),
7439
7897
  mcpServers: mcp,
7440
7898
  ...cliTransforms,
@@ -7482,7 +7940,7 @@ function runInstallStages(ctx, args) {
7482
7940
  ...outOfTrack.removed.length + outOfTrack.backedUp.length + outOfTrack.kept.length > 0 ? { outOfTrack } : {}
7483
7941
  };
7484
7942
  ctx.onProgress?.({ type: "baseline-complete", baseline });
7485
- const external = runExternalPhase(ctx);
7943
+ const { report: external, backups: externalSkillBackups } = runExternalPhase(ctx, previousLog);
7486
7944
  writeInstallLogSafe(
7487
7945
  ctx,
7488
7946
  // 링크 본문의 기준선도 같은 필드다 — 같은 경로면 뒤(이번에 쓴 값)가 이긴다.
@@ -7495,11 +7953,20 @@ function runInstallStages(ctx, args) {
7495
7953
  ...collectRootFiles(envFiles, ciScaffold, ledger.shared),
7496
7954
  // ADR-099 R2 — 파일째 사라진 첫 접촉 블록 모델 `AGENTS.md` 를 블록만 담아 다시 만들었다(기준선 밖) — uninstall 이
7497
7955
  // 블록을 걷고 남는 것이 없으면 파일째 지운다
7498
- ...cliSharedFiles.filter((f) => f.createdAsRoot === true).map((f) => ({ path: f.path, change: "created", notes: ["\uD558\uB124\uC2A4 \uBAAB\uB9CC(\uBE14\uB85D)"] }))
7956
+ ...cliSharedFiles.filter((f) => f.createdAsRoot === true).map((f) => ({ path: f.path, change: "created", notes: ["\uD558\uB124\uC2A4 \uBAAB\uB9CC(\uBE14\uB85D)"] })),
7957
+ // #761 — 외부 변환 · 링크 본문이 첫 접촉으로 비켜 둔 설치자 파일. 하네스가 떠날 때 되돌리는 근거다
7958
+ ...cliDisplaced,
7959
+ ...linked.displaced
7499
7960
  ],
7500
7961
  cliTransforms.codexOptIn
7501
7962
  );
7502
- return { ...baseline, external, staleHookRefs: [] };
7963
+ return {
7964
+ ...baseline,
7965
+ // #770 HIGH-1 — 외부 스킬을 다시 받으며 남긴 백업은 Phase 1 행 뒤에 생긴다 — 보고에도 싣는다
7966
+ ...externalSkillBackups.length > 0 ? { backups: [...baseline.backups ?? [], ...externalSkillBackups] } : {},
7967
+ external,
7968
+ staleHookRefs: []
7969
+ };
7503
7970
  }
7504
7971
  function legacyAnchorTrace(projectDir, spec) {
7505
7972
  if (!spec.cli.includes("claude")) return null;
@@ -7507,12 +7974,7 @@ function legacyAnchorTrace(projectDir, spec) {
7507
7974
  if (state !== "none" || traces.length < 2) return null;
7508
7975
  return traces.some((t) => t.path === LEGACY_ANCHOR_FILE) ? LEGACY_ANCHOR_FILE : null;
7509
7976
  }
7510
- function resolveUpdateBackupPath(ctx, claudeDir, previousLog) {
7511
- const claudeUntouched = !installedClis(previousLog).includes("claude");
7512
- if (!(ctx.backup ?? !claudeUntouched)) return null;
7513
- return copyBackupDir(claudeDir);
7514
- }
7515
- function runUpdateInstall(ctx, templatesDir, backupPath) {
7977
+ function runUpdateInstall(ctx, templatesDir) {
7516
7978
  const updateReport = runUpdateMode(
7517
7979
  ctx.projectDir,
7518
7980
  templatesDir,
@@ -7527,7 +7989,6 @@ function runUpdateInstall(ctx, templatesDir, backupPath) {
7527
7989
  baselineExcluded: [],
7528
7990
  baselineExcludedOnDisk: [],
7529
7991
  baselineForeignOwned: [],
7530
- backup: backupPath,
7531
7992
  installedTracks: [...ctx.spec.tracks].sort(),
7532
7993
  mcpServers: [],
7533
7994
  codex: null,
@@ -7557,7 +8018,7 @@ function releasedThisRun(args) {
7557
8018
  }
7558
8019
  if (id.startsWith(BASELINE_PREFIX)) {
7559
8020
  const again2 = manifest.some(
7560
- (e2) => e2.applies(manifestSpec) && classifyBaselineTarget(e2.target)?.id === id && existsSync21(join29(projectDir, e2.target))
8021
+ (e2) => e2.applies(manifestSpec) && classifyBaselineTarget(e2.target)?.id === id && existsSync22(join30(projectDir, e2.target))
7561
8022
  );
7562
8023
  return { id, again: again2, tail: "the harness manages it again" };
7563
8024
  }
@@ -7566,7 +8027,7 @@ function releasedThisRun(args) {
7566
8027
  options: spec.options,
7567
8028
  ...spec.userOverride ? { userOverride: spec.userOverride } : {}
7568
8029
  });
7569
- const recorded2 = (previousLog?.assets.some((a) => a.id === id) ?? false) || [".claude/skills", ".agents/skills"].some((d2) => existsSync21(join29(projectDir, d2, id)));
8030
+ const recorded2 = (previousLog?.assets.some((a) => a.id === id) ?? false) || [".claude/skills", ".agents/skills"].some((d2) => existsSync22(join30(projectDir, d2, id)));
7570
8031
  return {
7571
8032
  id,
7572
8033
  again,
@@ -7578,10 +8039,10 @@ function pendingKeyExcludes(projectDir, keyExclude, touched) {
7578
8039
  const done = new Set(touched);
7579
8040
  return keyExclude.flatMap((id) => {
7580
8041
  const path = sharedPathOfKeyId(id);
7581
- if (path === null || done.has(path) || !existsSync21(join29(projectDir, path))) return [];
8042
+ if (path === null || done.has(path) || !existsSync22(join30(projectDir, path))) return [];
7582
8043
  const name = adapterFor(path);
7583
8044
  const keys = [...excludedKeys(path, [id])];
7584
- const present2 = name === null ? null : ADAPTERS[name].read(readFileSync22(join29(projectDir, path), "utf8"), keys, projectDir);
8045
+ const present2 = name === null ? null : ADAPTERS[name].read(readFileSync22(join30(projectDir, path), "utf8"), keys, projectDir);
7585
8046
  return present2 !== null && present2.size === 0 ? [] : [{ id, path }];
7586
8047
  });
7587
8048
  }
@@ -7625,8 +8086,8 @@ function installCliNeutralAssets(manifestSpec, templatesDir, baselineExcluded, w
7625
8086
  continue;
7626
8087
  }
7627
8088
  if (!isCliNeutralTarget(entry.target)) continue;
7628
- const source = join29(templatesDir, entry.source);
7629
- if (!existsSync21(source)) {
8089
+ const source = join30(templatesDir, entry.source);
8090
+ if (!existsSync22(source)) {
7630
8091
  result.skipped += 1;
7631
8092
  continue;
7632
8093
  }
@@ -7646,14 +8107,14 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, baselineE
7646
8107
  if (!entry.applies(manifestSpec)) {
7647
8108
  continue;
7648
8109
  }
7649
- const target = join29(projectDir, entry.target);
8110
+ const target = join30(projectDir, entry.target);
7650
8111
  if (isBaselineExcluded(entry.target, baselineExcluded)) {
7651
8112
  result.excluded.push(entry.target);
7652
- if (existsSync21(target)) result.excludedOnDisk.push(entry.target);
8113
+ if (existsSync22(target)) result.excludedOnDisk.push(entry.target);
7653
8114
  continue;
7654
8115
  }
7655
- const source = join29(templatesDir, entry.source);
7656
- if (!existsSync21(source)) {
8116
+ const source = join30(templatesDir, entry.source);
8117
+ if (!existsSync22(source)) {
7657
8118
  result.skipped += 1;
7658
8119
  continue;
7659
8120
  }
@@ -7683,19 +8144,19 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, baselineE
7683
8144
  if (!result.foreignOwned.includes(slot)) result.foreignOwned.push(slot);
7684
8145
  continue;
7685
8146
  }
7686
- if (writer.skipOutside(join29(projectDir, entry.target, rel))) {
8147
+ if (writer.skipOutside(join30(projectDir, entry.target, rel))) {
7687
8148
  leftOutside += 1;
7688
8149
  continue;
7689
8150
  }
7690
- writer.harness(`${entry.target}/${rel}`, { source: join29(source, rel) });
8151
+ writer.harness(`${entry.target}/${rel}`, { source: join30(source, rel) });
7691
8152
  }
7692
8153
  if (leftOutside > 0) continue;
7693
8154
  result.dirsCopied += 1;
7694
8155
  }
7695
8156
  accumulateCategory(result.categories, entry);
7696
8157
  }
7697
- const hookDir = join29(projectDir, ".claude/hooks");
7698
- if (existsSync21(hookDir)) {
8158
+ const hookDir = join30(projectDir, ".claude/hooks");
8159
+ if (existsSync22(hookDir)) {
7699
8160
  chmodHooksSync(hookDir, projectDir);
7700
8161
  }
7701
8162
  writer.harness(
@@ -7711,12 +8172,12 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, baselineE
7711
8172
  if (settingsSource !== null) {
7712
8173
  writeSettingsPortion(writer, settingsSource, projectDir, previousLog, (script) => {
7713
8174
  const target = `${HOOKS_PREFIX}${script}`;
7714
- return manifest.some((e2) => e2.target === target && e2.applies(manifestSpec)) && !isBaselineExcluded(target, baselineExcluded) && existsSync21(join29(projectDir, target));
8175
+ return manifest.some((e2) => e2.target === target && e2.applies(manifestSpec)) && !isBaselineExcluded(target, baselineExcluded) && existsSync22(join30(projectDir, target));
7715
8176
  });
7716
8177
  result.keptHookRefs.push(
7717
8178
  ...findStaleHookRefs(
7718
- join29(projectDir, ".claude", "settings.json"),
7719
- join29(projectDir, ".claude")
8179
+ join30(projectDir, ".claude", "settings.json"),
8180
+ join30(projectDir, ".claude")
7720
8181
  )
7721
8182
  );
7722
8183
  }
@@ -7726,7 +8187,7 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, baselineE
7726
8187
  // ADR-085 · update(`upsertRootImport`)와 같은 판정 — 실제로 깔린 스킬만 안내한다. 밖 링크라 쓰지 않은
7727
8188
  // 스킬(ADR-098)을 "installed" 라 적으면 다음 update 가 그 절을 걷어 내 두 동작이 서로 반대로 쓴다.
7728
8189
  (manifestSpec.selectedInternalSkills ?? []).filter(
7729
- (id) => existsSync21(join29(projectDir, ".claude", "skills", id))
8190
+ (id) => existsSync22(join30(projectDir, ".claude", "skills", id))
7730
8191
  ),
7731
8192
  harnessRoot,
7732
8193
  writer
@@ -7765,9 +8226,9 @@ function writeEnvironmentFiles(writer, envExampleCreated, previousLog) {
7765
8226
  gitignoreNpxSkillsAdded: res.added.filter((line) => line !== ".env")
7766
8227
  };
7767
8228
  }
7768
- function runExternalPhase(ctx) {
8229
+ function runExternalPhase(ctx, previousLog) {
7769
8230
  if (ctx.runExternal === null) {
7770
- return null;
8231
+ return { report: null, backups: [] };
7771
8232
  }
7772
8233
  const { projectDir, spec } = ctx;
7773
8234
  const runExt = ctx.runExternal ?? runExternalInstall;
@@ -7788,17 +8249,24 @@ function runExternalPhase(ctx) {
7788
8249
  options: spec.options,
7789
8250
  ...spec.userOverride ? { userOverride: spec.userOverride } : {}
7790
8251
  };
7791
- const applicableCount = selectExternalTargets(EXTERNAL_ASSETS, {
7792
- ...filterCtx,
7793
- cli: spec.cli
7794
- }).targets.length;
7795
- ctx.onProgress?.({ type: "external-start", assetCount: applicableCount });
8252
+ const { targets } = selectExternalTargets(EXTERNAL_ASSETS, { ...filterCtx, cli: spec.cli });
8253
+ ctx.onProgress?.({ type: "external-start", assetCount: targets.length });
8254
+ const skillIds = new Set(targets.filter((a) => a.method.kind === "skill").map((a) => a.id));
8255
+ const settle = holdSkillSlots(
8256
+ projectDir,
8257
+ (previousLog?.assets ?? []).filter((a) => a.method === "skill" && skillIds.has(a.id))
8258
+ );
7796
8259
  const external = runExt(
7797
8260
  { ...filterCtx, cli: spec.cli, projectDir, ...spec.scope ? { scope: spec.scope } : {} },
7798
8261
  externalDeps
7799
8262
  );
7800
- ctx.onProgress?.({ type: "external-complete", report: external });
7801
- return external;
8263
+ const backups = settle();
8264
+ ctx.onProgress?.({
8265
+ type: "external-complete",
8266
+ report: external,
8267
+ ...backups.length > 0 ? { backups } : {}
8268
+ });
8269
+ return { report: external, backups };
7802
8270
  }
7803
8271
  var InstallInterruptedError = class extends Error {
7804
8272
  constructor(message, written, record, backups = [], hadInstall = false) {
@@ -7819,11 +8287,16 @@ function createInterruptJournal() {
7819
8287
  const shared2 = /* @__PURE__ */ new Map();
7820
8288
  const completed = /* @__PURE__ */ new Set();
7821
8289
  const backups = [];
8290
+ const displacedFiles = /* @__PURE__ */ new Map();
7822
8291
  const journal = {
7823
8292
  files,
7824
8293
  shared: shared2,
7825
8294
  completed,
7826
8295
  backups,
8296
+ displacedFiles,
8297
+ displaced: (f) => {
8298
+ if (!displacedFiles.has(f.path)) displacedFiles.set(f.path, f);
8299
+ },
7827
8300
  codexOptIn: null,
7828
8301
  trust: (report) => {
7829
8302
  journal.codexOptIn = report;
@@ -7875,7 +8348,7 @@ function recordInterruptedInstall(ctx, error, run) {
7875
8348
  ledger.anchor,
7876
8349
  previousLog,
7877
8350
  false,
7878
- [...rootFiles, ...ledger.rootFiles],
8351
+ [...rootFiles, ...ledger.rootFiles, ...journal.displacedFiles.values()],
7879
8352
  codexTrust
7880
8353
  );
7881
8354
  const path = writeInstallLog(
@@ -7998,9 +8471,9 @@ function accumulateCategory(cats, entry) {
7998
8471
  }
7999
8472
  }
8000
8473
  function writeRootClaudeMd(projectDir, tracks, continuousSkills, harnessRoot, writer) {
8001
- const target = join29(projectDir, "CLAUDE.md");
8474
+ const target = join30(projectDir, "CLAUDE.md");
8002
8475
  if (writer.skipOutside(target)) return null;
8003
- const existing = existsSync21(target) ? readFileSync22(target, "utf-8") : null;
8476
+ const existing = existsSync22(target) ? readFileSync22(target, "utf-8") : null;
8004
8477
  const seeded = existing === null ? seedRootClaudeProjectContext(projectDir, harnessRoot) : null;
8005
8478
  const content = upsertHarnessImport(existing, {
8006
8479
  projectName: basename4(projectDir),
@@ -8010,7 +8483,7 @@ function writeRootClaudeMd(projectDir, tracks, continuousSkills, harnessRoot, wr
8010
8483
  });
8011
8484
  if (content !== existing) {
8012
8485
  if (existing !== null) backupIfLossyUtf8(target);
8013
- writeFileSync8(target, content);
8486
+ writeFileSync9(target, content);
8014
8487
  }
8015
8488
  return {
8016
8489
  created: existing === null,
@@ -8034,8 +8507,8 @@ function listHookFiles(hookDir) {
8034
8507
  // src/commands/install-render.ts
8035
8508
  init_esm_shims();
8036
8509
  import { homedir as homedir3 } from "os";
8037
- import { existsSync as existsSync23, readdirSync as readdirSync5, readFileSync as readFileSync24 } from "fs";
8038
- import { join as join31, relative as relative4 } from "path";
8510
+ import { existsSync as existsSync24, readdirSync as readdirSync5, readFileSync as readFileSync24 } from "fs";
8511
+ import { join as join32, relative as relative4 } from "path";
8039
8512
 
8040
8513
  // src/preset-recommend.ts
8041
8514
  init_esm_shims();
@@ -8071,10 +8544,10 @@ function groupAssetsByCategory(assetIds) {
8071
8544
 
8072
8545
  // src/resident-entries.ts
8073
8546
  init_esm_shims();
8074
- import { existsSync as existsSync22, readFileSync as readFileSync23 } from "fs";
8075
- import { basename as basename5, join as join30 } from "path";
8547
+ import { existsSync as existsSync23, readFileSync as readFileSync23 } from "fs";
8548
+ import { basename as basename5, join as join31 } from "path";
8076
8549
  function residentEntries(spec) {
8077
- const skillThere = (id) => INTERNAL_BUNDLED_SKILL_IDS.includes(id) && [".claude/skills", ".agents/skills"].some((d2) => existsSync22(join30(spec.projectDir, d2, id)));
8550
+ const skillThere = (id) => INTERNAL_BUNDLED_SKILL_IDS.includes(id) && [".claude/skills", ".agents/skills"].some((d2) => existsSync23(join31(spec.projectDir, d2, id)));
8078
8551
  const forceExclude = (spec.userOverride?.forceExclude ?? []).filter((id) => !skillThere(id));
8079
8552
  const assetSpec = buildManifestSpec(
8080
8553
  spec.userOverride ? { ...spec, userOverride: { ...spec.userOverride, forceExclude } } : spec
@@ -8100,7 +8573,7 @@ function residentEntries(spec) {
8100
8573
  function residentCostFor(spec, entries = residentEntries(spec), contextFiles) {
8101
8574
  if (contextFiles !== void 0) return residentCost(entries, void 0, spec.cli, contextFiles);
8102
8575
  const cost = residentCost(entries, void 0, spec.cli, plannedClaudeContext(spec));
8103
- if (anchorSurface(spec.cli).scaffold === "agents-md" && existsSync22(join30(spec.projectDir, "AGENTS.md"))) {
8576
+ if (anchorSurface(spec.cli).scaffold === "agents-md" && existsSync23(join31(spec.projectDir, "AGENTS.md"))) {
8104
8577
  return makeResidentCost({
8105
8578
  ...cost,
8106
8579
  contextNote: "planned, not counting text already in AGENTS.md"
@@ -8111,8 +8584,8 @@ function residentCostFor(spec, entries = residentEntries(spec), contextFiles) {
8111
8584
  function plannedClaudeContext(spec) {
8112
8585
  if (!spec.cli.includes("claude")) return void 0;
8113
8586
  const root = resolveBundleRoot();
8114
- const rootClaude = join30(spec.projectDir, "CLAUDE.md");
8115
- const existing = existsSync22(rootClaude) ? readFileSync23(rootClaude, "utf-8") : null;
8587
+ const rootClaude = join31(spec.projectDir, "CLAUDE.md");
8588
+ const existing = existsSync23(rootClaude) ? readFileSync23(rootClaude, "utf-8") : null;
8116
8589
  const seeded = existing === null ? seedRootClaudeProjectContext(spec.projectDir, root) : null;
8117
8590
  if (existing === null && seeded === null) return void 0;
8118
8591
  const manifestSpec = buildManifestSpec(spec);
@@ -8122,14 +8595,14 @@ function plannedClaudeContext(spec) {
8122
8595
  continuousSkills: manifestSpec.selectedInternalSkills ?? [],
8123
8596
  ...seeded === null ? {} : { projectContext: seeded }
8124
8597
  });
8125
- return [join30(root, "templates", "CLAUDE.md"), { text }];
8598
+ return [join31(root, "templates", "CLAUDE.md"), { text }];
8126
8599
  }
8127
8600
  function onDiskTargets(spec, assetSpec) {
8128
8601
  const out = /* @__PURE__ */ new Set();
8129
8602
  for (const e2 of buildManifest(assetSpec)) {
8130
8603
  if (!e2.applies(assetSpec)) continue;
8131
8604
  const t = classifyBaselineTarget(e2.target);
8132
- if (t !== null && existsSync22(join30(spec.projectDir, e2.target))) out.add(t.id);
8605
+ if (t !== null && existsSync23(join31(spec.projectDir, e2.target))) out.add(t.id);
8133
8606
  }
8134
8607
  return out;
8135
8608
  }
@@ -8191,6 +8664,7 @@ function createInstallRenderer(log, spec, verbose) {
8191
8664
  log("");
8192
8665
  phase2HeaderPrinted = true;
8193
8666
  } else if (event.type === "external-complete") {
8667
+ for (const b2 of event.backups ?? []) log(assetRow("success", "backup", shortenPath(b2)));
8194
8668
  const excluded = event.report.excludedByCli;
8195
8669
  if (excluded.length > 0) {
8196
8670
  if (!phase2HeaderPrinted) {
@@ -8239,10 +8713,6 @@ function renderUpdateSummary(log, spec, report) {
8239
8713
  log("");
8240
8714
  log(infoRow("STATUS", c.green("Update complete")));
8241
8715
  log(infoRow("MODE", "update"));
8242
- if (report.backup) {
8243
- log(infoRow("BACKUP", shortenPath(report.backup)));
8244
- log(infoRow("ROLLBACK", `rm -rf .claude && mv ${shellQuotePath(report.backup)} .claude`));
8245
- }
8246
8716
  const cost = formatResidentCostLine(
8247
8717
  rulesInsideAgentsMd(
8248
8718
  spec,
@@ -8268,7 +8738,8 @@ function renderUpdateSummary(log, spec, report) {
8268
8738
  `${backups.length} file(s) ${how}, as *.backup-<time>${unmeasuredPart} \xB7 list: .uzys-agent-harness/update-backups.json`
8269
8739
  )
8270
8740
  );
8271
- if (removedBefore < backups.length)
8741
+ const displacedN = report.updateMode?.displaced?.length ?? 0;
8742
+ if (removedBefore + displacedN < backups.length)
8272
8743
  log(
8273
8744
  infoRow(
8274
8745
  "NEXT",
@@ -8283,23 +8754,23 @@ function residentEntriesOnDisk(spec) {
8283
8754
  const hasClaude = spec.cli.includes("claude");
8284
8755
  const surface = anchorSurface(spec.cli).scaffold;
8285
8756
  const mdFiles = (dir, kind, skip) => {
8286
- const abs = join31(spec.projectDir, dir);
8287
- return existsSync23(abs) ? readdirSync5(abs, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".md") && e2.name !== skip).map((e2) => ({
8757
+ const abs = join32(spec.projectDir, dir);
8758
+ return existsSync24(abs) ? readdirSync5(abs, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".md") && e2.name !== skip).map((e2) => ({
8288
8759
  source: `${kind}/${e2.name}`,
8289
8760
  // `target` 은 `residentCost` 의 분류 키다 — 실제 자리는 `file` 이 말한다.
8290
8761
  target: `.claude/${kind}/${e2.name}`,
8291
8762
  // 배포판이 아니라 **이 프로젝트의 파일**을 잰다 — 사용자가 고친 본문도,
8292
8763
  // 배포판에 더는 없는 파일도 실제로 상주하는 것은 이쪽이다.
8293
- file: join31(abs, e2.name)
8764
+ file: join32(abs, e2.name)
8294
8765
  })) : [];
8295
8766
  };
8296
- const rules = surface === "claude" ? mdFiles(join31(".claude", "rules"), "rules") : rulesInline(spec.cli) ? planned.filter((e2) => e2.target.startsWith(".claude/rules/")) : mdFiles(join31(".agents", "rules"), "rules", "uzys-harness.md");
8297
- const slot = hasClaude ? join31(".claude", "skills") : join31(".agents", "skills");
8767
+ const rules = surface === "claude" ? mdFiles(join32(".claude", "rules"), "rules") : rulesInline(spec.cli) ? planned.filter((e2) => e2.target.startsWith(".claude/rules/")) : mdFiles(join32(".agents", "rules"), "rules", "uzys-harness.md");
8768
+ const slot = hasClaude ? join32(".claude", "skills") : join32(".agents", "skills");
8298
8769
  const skills = planned.filter((e2) => e2.target.startsWith(".claude/skills/")).map((e2) => ({
8299
8770
  ...e2,
8300
- file: join31(spec.projectDir, slot, e2.target.slice(".claude/skills/".length))
8301
- })).filter((e2) => existsSync23(e2.file));
8302
- const agents = hasClaude ? mdFiles(join31(".claude", "agents"), "agents") : [];
8771
+ file: join32(spec.projectDir, slot, e2.target.slice(".claude/skills/".length))
8772
+ })).filter((e2) => existsSync24(e2.file));
8773
+ const agents = hasClaude ? mdFiles(join32(".claude", "agents"), "agents") : [];
8303
8774
  return [
8304
8775
  ...planned.filter(
8305
8776
  (e2) => !e2.target.startsWith(".claude/rules/") && !e2.target.startsWith(".claude/agents/") && !e2.target.startsWith(".claude/skills/")
@@ -8311,8 +8782,8 @@ function residentEntriesOnDisk(spec) {
8311
8782
  }
8312
8783
  function contextFilesOnDisk(spec) {
8313
8784
  const surface = anchorSurface(spec.cli).scaffold;
8314
- const files = surface === "claude" ? [HARNESS_ANCHOR_FILE, "CLAUDE.md", join31(".claude", "CLAUDE.md")] : ["AGENTS.md"];
8315
- return files.map((f) => join31(spec.projectDir, f));
8785
+ const files = surface === "claude" ? [HARNESS_ANCHOR_FILE, "CLAUDE.md", join32(".claude", "CLAUDE.md")] : ["AGENTS.md"];
8786
+ return files.map((f) => join32(spec.projectDir, f));
8316
8787
  }
8317
8788
  function rulesInsideAgentsMd(spec, cost) {
8318
8789
  if (anchorSurface(spec.cli).scaffold !== "agents-md" || !rulesInline(spec.cli)) return cost;
@@ -8566,9 +9037,9 @@ function renderFinalSummary(log, spec, report, fromWizard) {
8566
9037
  const codexTrusted = report.codexOptIn?.trustEntry.status === "registered" || report.codexOptIn?.trustEntry.status === "already-present";
8567
9038
  const alreadyTrustedGlobally = (() => {
8568
9039
  try {
8569
- const home = process.env.CODEX_HOME ?? join31(homedir3(), ".codex");
8570
- const configPath = join31(home, "config.toml");
8571
- if (!existsSync23(configPath)) return false;
9040
+ const home = process.env.CODEX_HOME ?? join32(homedir3(), ".codex");
9041
+ const configPath = join32(home, "config.toml");
9042
+ if (!existsSync24(configPath)) return false;
8572
9043
  return hasTrustEntry(readFileSync24(configPath, "utf8"), spec.projectDir);
8573
9044
  } catch {
8574
9045
  return false;
@@ -8594,7 +9065,7 @@ function renderFinalSummary(log, spec, report, fromWizard) {
8594
9065
  const remote = (report.shared ?? []).find((f) => f.path === ".mcp.json")?.remote ?? [];
8595
9066
  for (const line of remoteMcpNextLines(remote, spec.cli)) log(`${" ".repeat(16)} ${line}`);
8596
9067
  const scaffoldFiles = scaffoldFilesForCli(spec.cli).filter(
8597
- (f) => f === "CLAUDE.md" && report.rootClaudeMd?.created === true || hasFillPrompt(join31(spec.projectDir, f))
9068
+ (f) => f === "CLAUDE.md" && report.rootClaudeMd?.created === true || hasFillPrompt(join32(spec.projectDir, f))
8598
9069
  );
8599
9070
  if (scaffoldFiles.length > 0) {
8600
9071
  const auditInstalled = buildManifestSpec(spec).selectedInternalSkills?.includes("audit-harness-fit");
@@ -8664,12 +9135,25 @@ function linkedSkillRows(log, updated, notOurs) {
8664
9135
  );
8665
9136
  }
8666
9137
  }
9138
+ function hadFileRow(log, area, paths) {
9139
+ if (paths.length === 0) return;
9140
+ log(
9141
+ assetRow(
9142
+ "skip",
9143
+ `${area} had files with these names`,
9144
+ `${paths.length} backed up as *.backup-<time> \u2014 yours, put back on uninstall \xB7 ${paths.join(", ")}`
9145
+ )
9146
+ );
9147
+ }
8667
9148
  function renderPhase1Rows(log, baseline, verbose = false) {
8668
9149
  if (baseline.updateMode) {
8669
9150
  const noChecksum = new Set(baseline.updateMode.noChecksum ?? []);
8670
- if (baseline.backup) {
8671
- log(assetRow("success", "backup", shortenPath(baseline.backup)));
8672
- }
9151
+ const displaced = baseline.updateMode.displaced ?? [];
9152
+ const displacedSkills = displaced.filter((p2) => p2.startsWith(".claude/skills/"));
9153
+ const displacedPolicy = displaced.filter(
9154
+ (p2) => p2.startsWith(".claude/") && !p2.startsWith(".claude/skills/")
9155
+ );
9156
+ const displacedExternal = displaced.filter((p2) => !p2.startsWith(".claude/"));
8673
9157
  for (const [dir, count] of Object.entries(baseline.updateMode.updated)) {
8674
9158
  if (count > 0) log(assetRow("success", dir, `${count} files updated`));
8675
9159
  }
@@ -8803,6 +9287,7 @@ function renderPhase1Rows(log, baseline, verbose = false) {
8803
9287
  )
8804
9288
  );
8805
9289
  }
9290
+ hadFileRow(log, ".claude/skills", displacedSkills);
8806
9291
  if (baseline.updateMode.skillsPruned.length > 0) {
8807
9292
  log(
8808
9293
  assetRow(
@@ -8886,6 +9371,7 @@ function renderPhase1Rows(log, baseline, verbose = false) {
8886
9371
  )
8887
9372
  );
8888
9373
  }
9374
+ hadFileRow(log, "policy files", displacedPolicy);
8889
9375
  const unmeasured = [...noChecksum].filter((p2) => p2 !== ".mcp-allowlist");
8890
9376
  if (unmeasured.length > 0) {
8891
9377
  log(
@@ -8896,13 +9382,23 @@ function renderPhase1Rows(log, baseline, verbose = false) {
8896
9382
  )
8897
9383
  );
8898
9384
  }
9385
+ const retiredRefs = baseline.updateMode.retiredHookRefsRemoved ?? [];
9386
+ if (retiredRefs.length > 0) {
9387
+ log(
9388
+ assetRow(
9389
+ "success",
9390
+ "settings.json retired hook refs",
9391
+ `${retiredRefs.length} removed (harness wiring from an earlier release; script gone: ${retiredRefs.join(", ")})`
9392
+ )
9393
+ );
9394
+ }
8899
9395
  const staleRefs = baseline.updateMode.staleHookRefs;
8900
9396
  if (staleRefs.length > 0) {
8901
9397
  log(
8902
9398
  assetRow(
8903
9399
  "skip",
8904
9400
  "settings.json stale hook refs",
8905
- `${staleRefs.length} removed (${staleRefs.join(", ")})`
9401
+ `${staleRefs.length} left (yours \u2014 not in the install record; script missing: ${staleRefs.join(", ")})`
8906
9402
  )
8907
9403
  );
8908
9404
  }
@@ -8925,6 +9421,17 @@ function renderPhase1Rows(log, baseline, verbose = false) {
8925
9421
  )
8926
9422
  );
8927
9423
  }
9424
+ hadFileRow(log, "external CLI files", displacedExternal);
9425
+ const displacedSame = baseline.updateMode.displacedSame ?? [];
9426
+ if (displacedSame.length > 0) {
9427
+ log(
9428
+ assetRow(
9429
+ "skip",
9430
+ "same as the harness version",
9431
+ `${displacedSame.length} ${LINES.sameAsHarness} \xB7 ${displacedSame.join(", ")}`
9432
+ )
9433
+ );
9434
+ }
8928
9435
  if (baseline.updateMode.externalSkillsRefreshed > 0) {
8929
9436
  log(
8930
9437
  assetRow(
@@ -9092,9 +9599,6 @@ function renderPhase1Rows(log, baseline, verbose = false) {
9092
9599
  )
9093
9600
  );
9094
9601
  }
9095
- if (baseline.backup) {
9096
- log(assetRow("success", "backup", shortenPath(baseline.backup), TEMPLATES_COL));
9097
- }
9098
9602
  const shared2 = baseline.shared;
9099
9603
  if (shared2 !== void 0) {
9100
9604
  for (const f of shared2) {
@@ -9330,9 +9834,6 @@ function shortenPath(p2) {
9330
9834
  }
9331
9835
  return p2;
9332
9836
  }
9333
- function shellQuotePath(p2) {
9334
- return `'${p2.replace(/'/g, `'\\''`)}'`;
9335
- }
9336
9837
  function rootClaudeMdMeta(root, trackCount) {
9337
9838
  const tracks = `${trackCount} track${trackCount > 1 ? "s" : ""} noted`;
9338
9839
  if (!root.created) return `@import ${HARNESS_ANCHOR_FILE} (body preserved)`;
@@ -9616,16 +10117,6 @@ function executeSpec(requested, deps = {}) {
9616
10117
  exit(1);
9617
10118
  return;
9618
10119
  }
9619
- if (deps.mode === "update" && report.backup !== null) {
9620
- const liveClaude = join32(spec.projectDir, ".claude");
9621
- if (dirTreesIdentical(liveClaude, report.backup)) {
9622
- try {
9623
- rmSync4(report.backup, { recursive: true, force: true });
9624
- report.backup = null;
9625
- } catch {
9626
- }
9627
- }
9628
- }
9629
10120
  if (report.updateMode) {
9630
10121
  renderUpdateSummary(log, spec, report);
9631
10122
  return;
@@ -9704,7 +10195,7 @@ function registerInstallCommand(cli) {
9704
10195
 
9705
10196
  // src/commands/list.ts
9706
10197
  init_esm_shims();
9707
- import { existsSync as existsSync24, readFileSync as readFileSync25 } from "fs";
10198
+ import { existsSync as existsSync25, readFileSync as readFileSync25 } from "fs";
9708
10199
  import { join as join33, resolve as resolve3 } from "path";
9709
10200
  function listAction(options = {}, deps = {}) {
9710
10201
  const log = deps.log ?? console.log;
@@ -9809,7 +10300,7 @@ function formatHarnessFileRows(log, projectDir) {
9809
10300
  const rootMd = log.templates.rootClaudeMd;
9810
10301
  if (rootMd) {
9811
10302
  const path = join33(projectDir, rootMd.path);
9812
- const modified = existsSync24(path) && hashContent(readFileSync25(path, "utf8")) !== rootMd.sha256;
10303
+ const modified = existsSync25(path) && hashContent(readFileSync25(path, "utf8")) !== rootMd.sha256;
9813
10304
  rows.push(
9814
10305
  modified ? ` ${c.dim(rootMd.path)} ${c.yellow("(\uC218\uC815\uB428 \u2014 uninstall \uC2DC \uBCF4\uC874)")}` : ` ${c.dim(rootMd.path)}`
9815
10306
  );
@@ -9823,7 +10314,7 @@ var ROOT_FILE_CHANGE = {
9823
10314
  displaced: "\uBE44\uCF1C \uB460"
9824
10315
  };
9825
10316
  function formatRootFileRows(rootFiles, projectDir) {
9826
- const present2 = rootFiles.filter((f) => existsSync24(join33(projectDir, f.path)));
10317
+ const present2 = rootFiles.filter((f) => existsSync25(join33(projectDir, f.path)));
9827
10318
  const width = Math.max(0, ...present2.map((f) => f.path.length));
9828
10319
  return present2.map(
9829
10320
  (f) => ` ${c.dim(padDisplay(f.path, width))} ${c.dim(ROOT_FILE_CHANGE[f.change])} ${c.dim(f.notes.join(" / "))}`
@@ -9842,18 +10333,21 @@ init_esm_shims();
9842
10333
  import { spawnSync as spawnSync2 } from "child_process";
9843
10334
  import {
9844
10335
  accessSync,
9845
- existsSync as existsSync25,
10336
+ existsSync as existsSync26,
9846
10337
  constants as fsConstants,
9847
10338
  lstatSync as lstatSync5,
10339
+ mkdirSync as mkdirSync7,
9848
10340
  readdirSync as readdirSync6,
9849
10341
  readFileSync as readFileSync26,
10342
+ readlinkSync,
9850
10343
  realpathSync as realpathSync3,
10344
+ renameSync,
9851
10345
  rmdirSync as rmdirSync2,
9852
- rmSync as rmSync5,
9853
- statSync as statSync4,
9854
- writeFileSync as writeFileSync9
10346
+ rmSync as rmSync4,
10347
+ statSync as statSync5,
10348
+ writeFileSync as writeFileSync10
9855
10349
  } from "fs";
9856
- import { dirname as dirname6, join as join34, relative as relative5, resolve as resolve4, sep as sep5 } from "path";
10350
+ import { dirname as dirname7, join as join34, relative as relative5, resolve as resolve4, sep as sep5 } from "path";
9857
10351
 
9858
10352
  // src/uninstall-interactive.ts
9859
10353
  init_esm_shims();
@@ -11130,7 +11624,7 @@ var defaultPrompts = {
11130
11624
  {
11131
11625
  value: "hooks",
11132
11626
  label: "Hooks \xB7 settings.json",
11133
- hint: "stale hook refs cleaned"
11627
+ hint: "harness hook wiring \xB7 dead refs reported, not removed"
11134
11628
  },
11135
11629
  {
11136
11630
  value: "external",
@@ -11272,20 +11766,26 @@ function buildUninstallModeChoices(clis, assetCount) {
11272
11766
  {
11273
11767
  value: "selected",
11274
11768
  label: "Remove selected assets",
11275
- hint: assetCount > 0 ? `pick from the ${assetCount} external assets; templates (.claude/ etc.) stay` : "no external assets recorded",
11769
+ hint: assetCount > 0 ? `pick from the ${assetCount} external assets; harness files stay` : "no external assets recorded",
11276
11770
  enabled: assetCount > 0
11277
11771
  },
11278
11772
  {
11279
11773
  value: "all",
11280
11774
  label: "Remove everything",
11281
- hint: "assets + templates + the install record \u2014 CLI folders (.claude/ \xB7 .codex/ \xB7 .opencode/, whichever exist) are moved aside as <dir>.backup-<time>",
11775
+ hint: "assets + harness files + the install record \u2014 harness files are removed one by one; your own files in .claude/ \xB7 .codex/ \xB7 .opencode/ stay",
11282
11776
  enabled: true
11283
11777
  }
11284
11778
  ];
11285
11779
  }
11780
+ var CLI_FOLDERS = [".claude/", ".codex/", ".opencode/"];
11781
+ var FOLDER_CLI = {
11782
+ ".claude/": "claude",
11783
+ ".codex/": "codex",
11784
+ ".opencode/": "opencode"
11785
+ };
11286
11786
  function describeOwned(p2) {
11287
- if (p2.kind === "dir") {
11288
- return `${p2.path} \u2192 moved aside as ${p2.path.replace(/\/+$/, "")}.backup-<time> (your own files there stay in it)`;
11787
+ if (CLI_FOLDERS.includes(p2.path)) {
11788
+ return `${p2.path} harness files, one by one (your own files there stay)`;
11289
11789
  }
11290
11790
  if (p2.kind === "import-block") return `the import block in ${p2.path} (your text stays)`;
11291
11791
  if (p2.path === "AGENTS.md") return "AGENTS.md harness sections (your ## Project Context stays)";
@@ -11296,7 +11796,7 @@ function keptLinkLines(projectDir, dirs) {
11296
11796
  return dirs.flatMap((d2) => {
11297
11797
  const o = outsideLinkAt(projectDir, d2.replace(/\/+$/, ""));
11298
11798
  return o ? [
11299
- ` \xB7 ${o.link} stays \u2014 the link points outside the project (${o.link} \u2192 ${o.linkTarget}); not moved, nothing behind it removed`
11799
+ ` \xB7 ${o.link} stays \u2014 the link points outside the project (${o.link} \u2192 ${o.linkTarget}); nothing behind it is removed`
11300
11800
  ] : [];
11301
11801
  });
11302
11802
  }
@@ -11331,7 +11831,7 @@ function cliDirs(cli, installed) {
11331
11831
  cli,
11332
11832
  installed.filter((c3) => c3 !== cli)
11333
11833
  );
11334
- return [...exclusive, ...sharedNowUnowned].filter((p2) => p2.kind === "dir").map((p2) => p2.path);
11834
+ return [...exclusive, ...sharedNowUnowned].filter((p2) => CLI_FOLDERS.includes(p2.path)).map((p2) => p2.path);
11335
11835
  }
11336
11836
  function buildCliRows(installed) {
11337
11837
  return installed.map((cli) => {
@@ -11370,12 +11870,10 @@ async function runInteractiveUninstall(projectDir, deps = {}) {
11370
11870
  [
11371
11871
  "Remove everything? This is the same as: agent-harness uninstall --yes",
11372
11872
  ` \xB7 ${log.assets.length} recorded asset(s) \u2014 only those with an automatic reverse path are removed`,
11373
- " \xB7 templates: CLI folders (.claude/ \xB7 .codex/ \xB7 .opencode/, whichever exist) are moved aside as <dir>.backup-<time> (your own files there stay in them)",
11873
+ " \xB7 harness files are removed one by one; your own files in .claude/ \xB7 .codex/ \xB7 .opencode/ stay (a file of yours the harness had replaced is put back)",
11374
11874
  ...keptLinkLines(
11375
11875
  projectDir,
11376
- [log.templates?.claudeDir, log.templates?.codexDir, log.templates?.opencodeDir].filter(
11377
- (d2) => typeof d2 === "string"
11378
- )
11876
+ CLI_FOLDERS.filter((d2) => FOLDER_CLI[d2] !== void 0 && clis.includes(FOLDER_CLI[d2]))
11379
11877
  ),
11380
11878
  " \xB7 the install record goes too",
11381
11879
  " \xB7 files outside (.mcp.json etc.) are not deleted \u2014 you get instructions instead"
@@ -11472,14 +11970,9 @@ Proceed?`, initialValue: false });
11472
11970
  }
11473
11971
 
11474
11972
  // src/commands/uninstall.ts
11475
- function templateDirPath(projectDir, rel) {
11476
- return join34(projectDir, rel.replace(/\/+$/, ""));
11477
- }
11478
- function outsideTemplateLink(projectDir, rel) {
11479
- return outsideLinkAt(projectDir, rel.replace(/\/+$/, ""));
11480
- }
11973
+ var CODEX_CONFIG = ".codex/config.toml";
11481
11974
  function keptOutsideLinkLine(o, log) {
11482
- const head = ` ${c.yellow("\u2298")} ${o.link} \u2014 left as is: the link points outside the project (${o.link} \u2192 ${o.linkTarget}). Not moved, and nothing behind it is removed`;
11975
+ const head = ` ${c.yellow("\u2298")} ${o.link} \u2014 left as is: the link points outside the project (${o.link} \u2192 ${o.linkTarget}). Nothing behind it is removed`;
11483
11976
  const files = recordedUnder(log, o.link);
11484
11977
  if (files.length === 0) return `${head}; clean up harness files there by hand if intended.`;
11485
11978
  const shown = files.slice(0, KEPT_LINK_FILES_SHOWN).join(" \xB7 ");
@@ -11496,17 +11989,18 @@ function recordedUnder(log, link) {
11496
11989
  ];
11497
11990
  return [...new Set(paths.filter((p2) => p2.startsWith(`${link}/`)))].sort();
11498
11991
  }
11499
- function underKeptLink(path, kept) {
11500
- return kept.some((k2) => path.startsWith(`${k2.link}/`));
11501
- }
11502
- function movedAsideLine(projectDir, rel, backup) {
11503
- return ` ${status.success(`${rel} moved aside \u2192 ${relative5(projectDir, backup)} (your own files there are kept)`)}`;
11504
- }
11505
- var MOVE_ASIDE_PREVIEW = (rel) => ` \u25CB move ${rel} aside \u2192 ${rel.replace(/\/+$/, "")}.backup-<time> (your own files there stay in the backup)`;
11506
- function recordedTemplateDirs(log) {
11507
- return [log.templates?.claudeDir, log.templates?.codexDir, log.templates?.opencodeDir].filter(
11508
- (d2) => typeof d2 === "string"
11509
- );
11992
+ function keepTemplatesRemovedLines(log) {
11993
+ const ids = log.assets.filter((a) => a.scope === "project").map((a) => a.id);
11994
+ return [
11995
+ status.failure(
11996
+ c.red(
11997
+ "ERROR: --keep-templates was removed in v26.169.0 \u2014 harness files are now removed one by one and your own files in .claude/ \xB7 .codex/ \xB7 .opencode/ stay in place."
11998
+ )
11999
+ ),
12000
+ c.dim(
12001
+ ids.length > 0 ? ` To remove only the external assets: agent-harness uninstall --only ${ids.join(",")}` : " nothing on record for --only \u2014 run uninstall (everything) or uninstall --cli <name>"
12002
+ )
12003
+ ];
11510
12004
  }
11511
12005
  function uninstallAction(options, deps = {}) {
11512
12006
  const log = deps.log ?? console.log;
@@ -11515,7 +12009,6 @@ function uninstallAction(options, deps = {}) {
11515
12009
  const spawn = deps.spawn ?? defaultSpawn2;
11516
12010
  const rm = deps.rm ?? defaultRm;
11517
12011
  const writeLog = deps.writeLog ?? writeInstallLog;
11518
- const moveAside = deps.moveAside ?? backupDir;
11519
12012
  const harnessRoot = (deps.resolveHarnessRoot ?? defaultHarnessRoot)();
11520
12013
  const projectDir = resolve4(options.projectDir ?? process.cwd());
11521
12014
  const detected = detectInstallState(projectDir);
@@ -11525,10 +12018,15 @@ function uninstallAction(options, deps = {}) {
11525
12018
  exit(1);
11526
12019
  return;
11527
12020
  }
12021
+ if (options.keepTemplates) {
12022
+ for (const line of keepTemplatesRemovedLines(installLog)) err(line);
12023
+ exit(1);
12024
+ return;
12025
+ }
11528
12026
  if (options.cli !== void 0) {
11529
12027
  removeCliAction(
11530
12028
  { options, installLog, projectDir, harnessRoot },
11531
- { log, err, exit, rm, writeLog, moveAside }
12029
+ { log, err, exit, rm, writeLog }
11532
12030
  );
11533
12031
  return;
11534
12032
  }
@@ -11547,46 +12045,31 @@ function uninstallAction(options, deps = {}) {
11547
12045
  return;
11548
12046
  }
11549
12047
  const targetAssets = selectedIds ? installLog.assets.filter((a) => selectedIds.includes(a.id)) : installLog.assets;
11550
- const keepTemplates = options.keepTemplates || selectedIds !== null;
11551
- const movedDirs = keepTemplates ? [] : recordedTemplateDirs(installLog);
11552
- const sharedPaths = keepTemplates ? [] : [...CLI_SHARED, ...ROOT_SHARED];
12048
+ const assetsOnly = selectedIds !== null;
11553
12049
  const rootFiles = withSkillsLock(
11554
12050
  selectedIds ? [] : (installLog.rootFiles ?? []).filter(
11555
- (f) => !underAny(f.path, movedDirs) && !((ROOT_SHARED.includes(f.path) || CLI_SHARED.includes(f.path)) && !keepTemplates)
12051
+ (f) => !UNINSTALL_SHARED.includes(f.path) && f.change !== "displaced"
11556
12052
  ),
11557
12053
  targetAssets,
11558
12054
  projectDir
11559
12055
  );
11560
12056
  const globalTrust = selectedIds ? void 0 : installLog.codexTrust;
11561
- const templatesLog = {
11562
- ...installLog,
11563
- externalFiles: (installLog.externalFiles ?? []).filter(
11564
- (f) => !f.path.startsWith(`${INSTALL_LOG_DIR}/`)
11565
- )
11566
- };
11567
12057
  const journal = [];
11568
12058
  let filesRemoved = 0;
11569
12059
  const rmJ = (path) => {
11570
12060
  rm(path);
11571
12061
  filesRemoved += 1;
11572
12062
  };
11573
- const moveAsideJ = (path) => {
11574
- const backup = moveAside(path);
11575
- if (backup)
11576
- journal.push(`moved ${relative5(projectDir, path)} aside \u2192 ${relative5(projectDir, backup)}`);
11577
- return backup;
11578
- };
11579
- const plan = planReverse(targetAssets, { spawn, projectDir, rm: rmJ, movedDirs });
12063
+ const plan = planReverse(targetAssets, { spawn, projectDir, rm: rmJ });
11580
12064
  for (const line of headerLines(installLog, selectedIds, targetAssets.length)) log(line);
11581
12065
  if (options.dryRun) {
11582
12066
  for (const line of dryRunLines(
11583
12067
  plan,
11584
- templatesLog,
12068
+ installLog,
11585
12069
  projectDir,
11586
- keepTemplates,
12070
+ assetsOnly,
11587
12071
  rootFiles,
11588
12072
  harnessRoot,
11589
- sharedPaths,
11590
12073
  globalTrust
11591
12074
  )) {
11592
12075
  log(line);
@@ -11594,15 +12077,7 @@ function uninstallAction(options, deps = {}) {
11594
12077
  exit(0);
11595
12078
  return;
11596
12079
  }
11597
- const blocked = unwritablePaths({
11598
- projectDir,
11599
- installLog: templatesLog,
11600
- targetAssets,
11601
- selectedIds,
11602
- keepTemplates,
11603
- movedDirs,
11604
- sharedPaths
11605
- });
12080
+ const blocked = unwritablePaths({ projectDir, installLog, targetAssets, selectedIds });
11606
12081
  if (blocked.length > 0) {
11607
12082
  err(status.failure(c.red("ERROR: uninstall cannot write here \u2014 nothing was removed:")));
11608
12083
  for (const p2 of blocked) err(c.dim(` \xB7 ${p2 === "." ? "the project folder" : p2}`));
@@ -11614,32 +12089,21 @@ function uninstallAction(options, deps = {}) {
11614
12089
  let reversed;
11615
12090
  try {
11616
12091
  reversed = executeReverse(plan, log, logSurvives, journal);
11617
- if (!keepTemplates) {
11618
- const sharedStrips = stripCliShared(installLog, projectDir, harnessRoot, sharedPaths, true);
11619
- for (const r of sharedStrips) {
11620
- if (r.removed.length > 0) journal.push(`removed the harness part from ${r.path}`);
11621
- }
11622
- const { rootClaudeMdKept, importStripped, external, moved, keptLinks } = removeTemplates(
11623
- templatesLog,
11624
- projectDir,
11625
- { rm: rmJ, moveAside: moveAsideJ },
12092
+ if (!assetsOnly) {
12093
+ const done = removeHarnessFiles(installLog, projectDir, FULL_SCOPE, {
12094
+ rm: rmJ,
12095
+ write: true,
11626
12096
  harnessRoot
11627
- );
11628
- for (const m of moved) log(movedAsideLine(projectDir, m.rel, m.backup));
11629
- for (const o of keptLinks) log(keptOutsideLinkLine(o, installLog));
11630
- if (recordedTemplateDirs(installLog).length === 0) {
11631
- log(` ${status.success("templates removed: (none)")}`);
12097
+ });
12098
+ for (const r of done.shared) {
12099
+ if (r.removed.length > 0 || r.fileRemoved)
12100
+ journal.push(`removed the harness part from ${r.path}`);
11632
12101
  }
11633
- for (const line of externalRemovalLines(external)) log(line);
11634
- for (const line of sharedStripLines(sharedStrips, false)) log(line);
11635
- if (importStripped) {
11636
- log(` ${status.success("CLAUDE.md \u2014 harness @import removed (\uBCF8\uBB38 \uBCF4\uC874)")}`);
12102
+ for (const d2 of done.displaced) {
12103
+ if (d2.verdict === "restore") journal.push(`put back ${d2.path}`);
11637
12104
  }
11638
- if (rootClaudeMdKept) {
11639
- const kept = installLog.templates.rootClaudeMd?.path ?? "CLAUDE.md";
11640
- log(
11641
- ` ${c.yellow("\u2298")} ${kept} kept \u2014 modified since install. Remove manually if intended.`
11642
- );
12105
+ for (const line of harnessRemovalLines(done, installLog, projectDir, FULL_SCOPE, false)) {
12106
+ log(line);
11643
12107
  }
11644
12108
  }
11645
12109
  } catch (e2) {
@@ -11664,26 +12128,20 @@ function uninstallAction(options, deps = {}) {
11664
12128
  { installLog, projectDir, selectedIds, removedIds },
11665
12129
  { log, err, rm, writeLog }
11666
12130
  );
11667
- for (const line of advisoryLines(
11668
- plan,
11669
- projectDir,
11670
- rootFiles,
11671
- options.keepTemplates === true,
11672
- globalTrust
11673
- )) {
12131
+ for (const line of advisoryLines(plan, projectDir, rootFiles, globalTrust)) {
11674
12132
  log(line);
11675
12133
  }
11676
12134
  const outcome = summarize({
11677
12135
  succeeded,
11678
12136
  failed,
11679
12137
  logWriteFailed,
11680
- // 제거된 것도 없고 templates 도 안 지웠는데, 사용자에게 줄 방법조차 없을 때만 실패다.
12138
+ // 제거된 것도 없고 하네스 파일도 안 건드렸는데, 사용자에게 줄 방법조차 없을 때만 실패다.
11681
12139
  // ① `--only` = "이걸 빼라"는 특정 요청 — 하나도 못 뺐으면 요청 불이행 (C1).
11682
12140
  // ② 자동 경로가 없는 자산이 남았으면 — 안내할 명령조차 없다 (R1).
11683
12141
  // ③ 반면 global 자산만 남은 경우는 **정확한 수기 명령을 출력했으므로** 성공이다.
11684
12142
  // D16 설계대로의 정상이고, 여기까지 묶으면 global scope 사용자는 exit 0 을 영원히
11685
12143
  // 못 받는다 (로그가 지워져 재시도하면 더 나빠진다).
11686
- nothingDone: succeeded === 0 && keepTemplates && (logSurvives || plan.noReversePath.length > 0)
12144
+ nothingDone: succeeded === 0 && assetsOnly && (logSurvives || plan.noReversePath.length > 0)
11687
12145
  });
11688
12146
  log("");
11689
12147
  log(outcome.line);
@@ -11700,18 +12158,19 @@ function unwritablePaths(ctx) {
11700
12158
  if (!safeExists(abs)) return;
11701
12159
  const real = safeRealpath(abs);
11702
12160
  if (real !== null && isOutsideProject(projectDir, real)) return;
11703
- need.add(dirname6(path));
12161
+ need.add(dirname7(path));
11704
12162
  };
11705
12163
  for (const a of project)
11706
12164
  if (a.method === "skill") for (const f of a.files ?? []) parentOf(f.path);
11707
- if (!ctx.keepTemplates) {
12165
+ if (ctx.selectedIds === null) {
11708
12166
  const recordedShared = new Set((installLog.portions ?? []).map((p2) => p2.path));
11709
- for (const p2 of ctx.sharedPaths) {
11710
- if (recordedShared.has(p2) && existsSync25(join34(projectDir, p2))) need.add(p2);
11711
- }
11712
- for (const f of installLog.externalFiles ?? []) {
11713
- if (!underAny(f.path, ctx.movedDirs)) parentOf(f.path);
12167
+ const made = new Set((installLog.externalFiles ?? []).map((f) => f.path));
12168
+ for (const p2 of UNINSTALL_SHARED) {
12169
+ if (recordedShared.has(p2) && existsSync26(join34(projectDir, p2))) need.add(p2);
12170
+ if (made.has(p2)) parentOf(p2);
11714
12171
  }
12172
+ for (const p2 of harnessCandidates(installLog)) parentOf(p2);
12173
+ for (const f of installLog.rootFiles ?? []) if (f.change === "displaced") parentOf(f.path);
11715
12174
  if (hasRootImport(projectDir)) need.add("CLAUDE.md");
11716
12175
  }
11717
12176
  return [...need].filter((p2) => {
@@ -11801,14 +12260,15 @@ function settleLog(ctx, io) {
11801
12260
  }
11802
12261
  } else {
11803
12262
  io.rm(join34(projectDir, INSTALL_LOG_DIR));
11804
- if (outsideTemplateLink(projectDir, LEGACY_INSTALL_LOG_DIR) === null) {
12263
+ if (outsideLinkAt(projectDir, LEGACY_INSTALL_LOG_DIR) === null) {
11805
12264
  io.rm(legacyInstallLogPath(projectDir));
12265
+ pruneEmptyDirsUpward(projectDir, join34(projectDir, LEGACY_INSTALL_LOG_DIR));
11806
12266
  }
11807
12267
  io.log(` ${status.success("install log removed")}`);
11808
12268
  }
11809
12269
  return false;
11810
12270
  }
11811
- function dryRunLines(plan, installLog, projectDir, keepTemplates, rootFiles, harnessRoot, sharedPaths, globalTrust) {
12271
+ function dryRunLines(plan, installLog, projectDir, assetsOnly, rootFiles, harnessRoot, globalTrust) {
11812
12272
  const lines2 = [c.yellow("[DRY RUN] reverse list (\uC2E4\uC81C \uBCC0\uACBD \uC5C6\uC74C):"), ""];
11813
12273
  if (plan.reverseSteps.length === 0) {
11814
12274
  lines2.push(c.dim(" (no project-scope assets to reverse)"));
@@ -11821,35 +12281,19 @@ function dryRunLines(plan, installLog, projectDir, keepTemplates, rootFiles, har
11821
12281
  ...(plan.hints.get(a.id) ?? []).map((h2) => c.dim(` \u21B3 ${h2}`))
11822
12282
  ])
11823
12283
  );
11824
- if (!keepTemplates) {
11825
- const dirs = recordedTemplateDirs(installLog);
11826
- for (const rel of dirs) {
11827
- const kept = outsideTemplateLink(projectDir, rel);
11828
- if (kept) lines2.push(keptOutsideLinkLine(kept, installLog));
11829
- else if (existsSync25(join34(projectDir, rel))) lines2.push(MOVE_ASIDE_PREVIEW(rel));
11830
- }
11831
- if (dirs.length === 0) lines2.push(" \u25CB remove templates: (none)");
11832
- const rootMd = installLog.templates.rootClaudeMd;
11833
- if (rootMd) {
11834
- lines2.push(
11835
- rootClaudeMdModified(installLog, projectDir) ? ` \u25CB keep ${rootMd.path} (modified since install \u2014 preserved)` : ` \u25CB remove ${rootMd.path}`
11836
- );
11837
- }
11838
- if (hasRootImport(projectDir)) {
11839
- lines2.push(" \u25CB strip harness @import from CLAUDE.md (\uBCF8\uBB38 \uBCF4\uC874)");
11840
- }
11841
- lines2.push(...previewExternalLines(installLog, projectDir, harnessRoot, dirs));
11842
- lines2.push(
11843
- ...sharedStripLines(
11844
- stripCliShared(installLog, projectDir, harnessRoot, sharedPaths, false),
11845
- true
11846
- )
11847
- );
12284
+ if (!assetsOnly) {
12285
+ const preview = removeHarnessFiles(installLog, projectDir, FULL_SCOPE, {
12286
+ rm: () => {
12287
+ },
12288
+ write: false,
12289
+ harnessRoot
12290
+ });
12291
+ lines2.push(...harnessRemovalLines(preview, installLog, projectDir, FULL_SCOPE, true));
11848
12292
  }
11849
- lines2.push(...advisoryLines(plan, projectDir, rootFiles, keepTemplates, globalTrust), "");
12293
+ lines2.push(...advisoryLines(plan, projectDir, rootFiles, globalTrust), "");
11850
12294
  return lines2;
11851
12295
  }
11852
- function advisoryLines(plan, projectDir, rootFiles, keepTemplates = false, globalTrust) {
12296
+ function advisoryLines(plan, projectDir, rootFiles, globalTrust) {
11853
12297
  const lines2 = [];
11854
12298
  if (globalTrust) {
11855
12299
  lines2.push(
@@ -11870,24 +12314,25 @@ function advisoryLines(plan, projectDir, rootFiles, keepTemplates = false, globa
11870
12314
  lines2.push(c.dim(` \xB7 ${adv.asset.id} (${adv.asset.method})`), c.dim(` ${adv.command}`));
11871
12315
  }
11872
12316
  }
11873
- lines2.push(...rootFileAdvisoryLines(rootFiles, projectDir, keepTemplates));
12317
+ lines2.push(...rootFileAdvisoryLines(rootFiles, projectDir));
11874
12318
  lines2.push(...backupArtifactAdvisoryLines(projectDir));
11875
12319
  return lines2;
11876
12320
  }
11877
12321
  function backupArtifactAdvisoryLines(projectDir) {
11878
- const stampRe = /\.backup-\d{8}T\d{6}$/;
11879
12322
  const found = [];
11880
12323
  for (const e2 of readdirSync6(projectDir, { withFileTypes: true })) {
11881
- if ([".claude", ".codex", ".opencode"].some((d2) => e2.name.startsWith(`${d2}.backup-`))) {
12324
+ if (CLI_FOLDERS2.some((d2) => e2.name.startsWith(`${d2}.backup-`))) {
11882
12325
  found.push(e2.name);
11883
- } else if (e2.isFile() && stampRe.test(e2.name)) {
12326
+ } else if (e2.isFile() && BACKUP_SUFFIX.test(e2.name)) {
11884
12327
  found.push(e2.name);
11885
12328
  }
11886
12329
  }
11887
- const agents = join34(projectDir, ".agents");
11888
- if (existsSync25(agents)) {
11889
- for (const rel of listFilesRecursive(agents)) {
11890
- if (stampRe.test(rel)) found.push(`.agents/${rel}`);
12330
+ for (const dir of BACKUP_SCAN_DIRS) {
12331
+ if (dir === INSTALL_LOG_DIR) continue;
12332
+ const abs = join34(projectDir, dir);
12333
+ if (!existsSync26(abs) || outsideLinkAt(projectDir, dir) !== null) continue;
12334
+ for (const rel of listFilesRecursive(abs)) {
12335
+ if (BACKUP_SUFFIX.test(rel)) found.push(`${dir}/${rel}`);
11891
12336
  }
11892
12337
  }
11893
12338
  if (found.length === 0) return [];
@@ -11898,9 +12343,9 @@ function backupArtifactAdvisoryLines(projectDir) {
11898
12343
  ...found.sort().map((f) => c.dim(` ${f}`))
11899
12344
  ];
11900
12345
  }
11901
- function rootFileAdvisoryLines(rootFiles, projectDir, keepTemplates = false) {
11902
- const header = keepTemplates ? "[LEFT] uninstall \uD6C4 \uB0A8\uB294 \uAC83 \u2014 templates \uBCF4\uC874(.claude/ \uB4F1) + \uB8E8\uD2B8 \uD30C\uC77C (\uC790\uB3D9\uC73C\uB85C \uC9C0\uC6B0\uC9C0 \uC54A\uB294\uB2E4):" : "[ROOT] `.claude/` \uBC16\uC5D0 \uB0A8\uB294 \uAC83 (\uC790\uB3D9\uC73C\uB85C \uC9C0\uC6B0\uC9C0 \uC54A\uB294\uB2E4):";
11903
- const present2 = rootFiles.filter((f) => existsSync25(join34(projectDir, f.path)));
12346
+ function rootFileAdvisoryLines(rootFiles, projectDir) {
12347
+ const header = "[ROOT] `.claude/` \uBC16\uC5D0 \uB0A8\uB294 \uAC83 (\uC790\uB3D9\uC73C\uB85C \uC9C0\uC6B0\uC9C0 \uC54A\uB294\uB2E4):";
12348
+ const present2 = rootFiles.filter((f) => existsSync26(join34(projectDir, f.path)));
11904
12349
  if (present2.length === 0) return [];
11905
12350
  const outside = /* @__PURE__ */ new Map();
11906
12351
  const inside = present2.filter((f) => {
@@ -11943,7 +12388,7 @@ function rootFileMeaning(f, projectDir) {
11943
12388
  case "displaced": {
11944
12389
  const backup = f.notes[0];
11945
12390
  if (backup === void 0) return "\uD558\uB124\uC2A4 \uD310\uACFC \uAC19\uC740 \uC124\uCE58\uC790 \uD30C\uC77C\uC774 \uC788\uB358 \uC790\uB9AC (\uADF8\uB300\uB85C \uB450\uC5C8\uB2E4)";
11946
- return existsSync25(join34(projectDir, backup)) ? `\uD558\uB124\uC2A4\uAC00 \uC4F4 \uC790\uB9AC \u2014 \uC6D0\uB798 \uC788\uB358 \uC124\uCE58\uC790 \uD30C\uC77C\uC740 ${backup} \uC5D0 \uC788\uB2E4` : `\uD558\uB124\uC2A4\uAC00 \uC4F4 \uC790\uB9AC \u2014 \uC6D0\uB798 \uC788\uB358 \uC124\uCE58\uC790 \uD30C\uC77C\uC758 \uBC31\uC5C5(${backup})\uC774 \uB354\uB294 \uC5C6\uB2E4`;
12391
+ return existsSync26(join34(projectDir, backup)) ? `\uD558\uB124\uC2A4\uAC00 \uC4F4 \uC790\uB9AC \u2014 \uC6D0\uB798 \uC788\uB358 \uC124\uCE58\uC790 \uD30C\uC77C\uC740 ${backup} \uC5D0 \uC788\uB2E4` : `\uD558\uB124\uC2A4\uAC00 \uC4F4 \uC790\uB9AC \u2014 \uC6D0\uB798 \uC788\uB358 \uC124\uCE58\uC790 \uD30C\uC77C\uC758 \uBC31\uC5C5(${backup})\uC774 \uB354\uB294 \uC5C6\uB2E4`;
11947
12392
  }
11948
12393
  }
11949
12394
  }
@@ -11954,8 +12399,8 @@ var TEMPLATE_DIR_FIELD = {
11954
12399
  };
11955
12400
  function resolveCliTarget(ctx, io) {
11956
12401
  const { options, installLog } = ctx;
11957
- if (options.only !== void 0 || options.keepTemplates) {
11958
- io.err(status.failure(c.red("ERROR: --cli \uB294 --only \xB7 --keep-templates \uC640 \uD568\uAED8 \uC4F8 \uC218 \uC5C6\uB2E4")));
12402
+ if (options.only !== void 0) {
12403
+ io.err(status.failure(c.red("ERROR: --cli \uB294 --only \uC640 \uD568\uAED8 \uC4F8 \uC218 \uC5C6\uB2E4")));
11959
12404
  io.err(
11960
12405
  c.dim(
11961
12406
  " CLI \uD558\uB098 \uC81C\uAC70: uninstall --cli <name> / \uC790\uC0B0 \uD558\uB098 \uC81C\uAC70: uninstall --only <id>"
@@ -11989,20 +12434,24 @@ function resolveCliTarget(ctx, io) {
11989
12434
  function underAny(path, owned) {
11990
12435
  return owned.some((o) => o.endsWith("/") ? path.startsWith(o) : path === o);
11991
12436
  }
11992
- function planCliRemoval(target, remaining, installLog) {
12437
+ function planCliRemoval(target, remaining) {
11993
12438
  const { exclusive, sharedNowUnowned } = removableFor(target, remaining);
11994
12439
  const owned = [...exclusive, ...sharedNowUnowned];
11995
- const recorded2 = owned.filter((o) => o.kind === "recorded").map((o) => o.path);
11996
- const scopedFiles = (installLog.externalFiles ?? []).filter(
11997
- (f) => underAny(f.path, recorded2)
11998
- );
12440
+ const prefixes = owned.filter((o) => o.kind === "recorded" || o.kind === "anchor").map((o) => o.path);
12441
+ const has = (path) => underAny(path, prefixes);
11999
12442
  return {
12000
12443
  target,
12001
- dirs: owned.filter((o) => o.kind === "dir").map((o) => o.path),
12002
- shared: CLI_SHARED.filter((p2) => recorded2.includes(p2)),
12003
- scoped: { ...installLog, externalFiles: scopedFiles },
12004
- anchor: owned.some((o) => o.kind === "anchor"),
12005
- importBlock: owned.some((o) => o.kind === "import-block")
12444
+ scope: {
12445
+ has,
12446
+ // `.claude/settings.json` 은 claude 의 `.claude/` 아래 · `.codex/config.toml` 은 codex 의 `.codex/` 아래 · `AGENTS.md` ·
12447
+ // `opencode.json` 은 마지막 사용자일 때만 표에 든다. `.mcp.json`(`keep`) · `.gitignore` 는 CLI 하나로 빠지지 않는다
12448
+ shared: UNINSTALL_SHARED.filter(has),
12449
+ // claude 를 뺄 때 `.claude/` 아래 다섯 — `docs/decisions` 는 CLI 소속이 아니다
12450
+ skeleton: PROJECT_SKELETON_DIRS.filter(has),
12451
+ folders: CLI_FOLDERS2.filter((d2) => has(`${d2}/`)),
12452
+ importBlock: owned.some((o) => o.kind === "import-block"),
12453
+ anchor: owned.some((o) => o.kind === "anchor")
12454
+ }
12006
12455
  };
12007
12456
  }
12008
12457
  function removeCliHeader(target, installed, remaining) {
@@ -12019,27 +12468,14 @@ function removeCliHeader(target, installed, remaining) {
12019
12468
  function removeCliDryRunLines(plan, ctx, remaining) {
12020
12469
  const { installLog, projectDir, harnessRoot } = ctx;
12021
12470
  const lines2 = [c.yellow("[DRY RUN] CLI \uC81C\uAC70 \uBBF8\uB9AC\uBCF4\uAE30 (\uC2E4\uC81C \uBCC0\uACBD \uC5C6\uC74C):"), ""];
12022
- for (const dir of plan.dirs) {
12023
- const kept = outsideTemplateLink(projectDir, dir);
12024
- if (kept) lines2.push(keptOutsideLinkLine(kept, installLog));
12025
- else if (existsSync25(join34(projectDir, dir))) lines2.push(MOVE_ASIDE_PREVIEW(dir));
12026
- }
12027
- lines2.push(...previewExternalLines(plan.scoped, projectDir, harnessRoot));
12028
- lines2.push(
12029
- ...sharedStripLines(
12030
- stripCliShared(installLog, projectDir, harnessRoot, plan.shared, false),
12031
- true
12032
- )
12033
- );
12034
- const rootMd = plan.anchor ? installLog.templates.rootClaudeMd : void 0;
12035
- if (rootMd && existsSync25(join34(projectDir, rootMd.path))) {
12036
- lines2.push(
12037
- rootClaudeMdModified(installLog, projectDir) ? ` \u25CB keep ${rootMd.path} (modified since install \u2014 preserved)` : ` \u25CB remove ${rootMd.path}`
12038
- );
12039
- }
12040
- if (plan.importBlock && hasRootImport(projectDir)) {
12041
- lines2.push(" \u25CB strip harness @import from CLAUDE.md (\uBCF8\uBB38 \uBCF4\uC874)");
12042
- }
12471
+ const preview = removeHarnessFiles(installLog, projectDir, plan.scope, {
12472
+ rm: () => {
12473
+ },
12474
+ write: false,
12475
+ harnessRoot
12476
+ });
12477
+ lines2.push(...harnessRemovalLines(preview, installLog, projectDir, plan.scope, true));
12478
+ lines2.push(...assetsLeftLines(installLog, projectDir, plan.scope));
12043
12479
  const rerender = agentsMdRerenderSet(plan.target, remaining, installLog, projectDir);
12044
12480
  if (rerender !== null) {
12045
12481
  lines2.push(` \u25CB would re-render AGENTS.md for ${displayNames(rerender)} (your sections kept)`);
@@ -12055,7 +12491,7 @@ function agentsMdRerenderSet(target, remaining, log, projectDir) {
12055
12491
  const set = agentsMdClis(remaining);
12056
12492
  if (set.length === 0) return null;
12057
12493
  const recorded2 = (log.externalFiles ?? []).some((f) => f.path === AGENTS_MD2) || (log.portions ?? []).some((p2) => p2.path === AGENTS_MD2);
12058
- if (!recorded2 || !existsSync25(join34(projectDir, AGENTS_MD2))) return null;
12494
+ if (!recorded2 || !existsSync26(join34(projectDir, AGENTS_MD2))) return null;
12059
12495
  return set;
12060
12496
  }
12061
12497
  function rerenderAgentsMd(ctx, set) {
@@ -12096,21 +12532,7 @@ function rerenderLines(r) {
12096
12532
  }
12097
12533
  return lines2;
12098
12534
  }
12099
- function removeHarnessAnchor(ctx, io) {
12100
- const { installLog, projectDir } = ctx;
12101
- const rootMd = installLog.templates.rootClaudeMd;
12102
- if (!rootMd || !existsSync25(join34(projectDir, rootMd.path))) return false;
12103
- if (rootClaudeMdModified(installLog, projectDir)) {
12104
- io.log(
12105
- ` ${c.yellow("\u2298")} ${rootMd.path} kept \u2014 modified since install. Remove manually if intended.`
12106
- );
12107
- return false;
12108
- }
12109
- io.rm(join34(projectDir, rootMd.path));
12110
- io.log(` ${status.success(`${rootMd.path} removed`)}`);
12111
- return true;
12112
- }
12113
- function settleCliLog(ctx, target, remaining, recovered, anchorRemoved, removedDirs, shared2) {
12535
+ function settleCliLog(ctx, target, remaining, done, anchorRemoved) {
12114
12536
  const { installLog } = ctx;
12115
12537
  const next = {
12116
12538
  ...installLog,
@@ -12123,23 +12545,26 @@ function settleCliLog(ctx, target, remaining, recovered, anchorRemoved, removedD
12123
12545
  },
12124
12546
  templates: { ...installLog.templates }
12125
12547
  };
12126
- next.assets = installLog.assets.map(
12127
- (a) => a.files === void 0 ? a : { ...a, files: a.files.filter((f) => !underAny(f.path, removedDirs)) }
12128
- );
12129
12548
  const field = TEMPLATE_DIR_FIELD[target];
12130
12549
  if (field) delete next.templates[field];
12131
12550
  if (anchorRemoved) delete next.templates.rootClaudeMd;
12132
- const survivors = (installLog.externalFiles ?? []).filter(
12133
- (f) => !recovered.has(f.path) && !underAny(f.path, removedDirs)
12134
- );
12551
+ if (target === "claude") {
12552
+ delete next.policyFiles;
12553
+ delete next.skillFiles;
12554
+ }
12555
+ const survivors = (installLog.externalFiles ?? []).filter((f) => !done.forgotten.has(f.path));
12135
12556
  if (survivors.length > 0) next.externalFiles = survivors;
12136
12557
  else delete next.externalFiles;
12137
- const touched = new Set(shared2.map((r) => r.path));
12558
+ const settled = new Set(done.displaced.filter((d2) => d2.forget).map((d2) => d2.path));
12559
+ if (installLog.rootFiles !== void 0) {
12560
+ next.rootFiles = installLog.rootFiles.filter(
12561
+ (f) => !(f.change === "displaced" && settled.has(f.path))
12562
+ );
12563
+ }
12564
+ const touched = new Set(done.shared.map((r) => r.path));
12138
12565
  const portions = [
12139
- ...(installLog.portions ?? []).filter(
12140
- (p2) => !touched.has(p2.path) && !underAny(p2.path, removedDirs)
12141
- ),
12142
- ...shared2.flatMap((r) => r.portions)
12566
+ ...(installLog.portions ?? []).filter((p2) => !touched.has(p2.path)),
12567
+ ...done.shared.flatMap((r) => r.portions)
12143
12568
  ];
12144
12569
  if (portions.length > 0) next.portions = portions;
12145
12570
  else delete next.portions;
@@ -12151,36 +12576,26 @@ function removeCliAction(ctx, io) {
12151
12576
  const { options, installLog, projectDir, harnessRoot } = ctx;
12152
12577
  const installed = installedClis(installLog);
12153
12578
  const remaining = installed.filter((cli) => cli !== target);
12154
- const plan = planCliRemoval(target, remaining, installLog);
12579
+ const plan = planCliRemoval(target, remaining);
12155
12580
  for (const line of removeCliHeader(target, installed, remaining)) io.log(line);
12156
12581
  if (options.dryRun) {
12157
12582
  for (const line of removeCliDryRunLines(plan, ctx, remaining)) io.log(line);
12158
12583
  io.exit(0);
12159
12584
  return;
12160
12585
  }
12161
- for (const dir of plan.dirs) {
12162
- const kept = outsideTemplateLink(projectDir, dir);
12163
- if (kept) {
12164
- io.log(keptOutsideLinkLine(kept, installLog));
12165
- continue;
12166
- }
12167
- if (!existsSync25(join34(projectDir, dir))) continue;
12168
- const backup = io.moveAside(templateDirPath(projectDir, dir));
12169
- if (backup) io.log(movedAsideLine(projectDir, dir, backup));
12170
- }
12171
- const external = removeExternalFiles(plan.scoped, projectDir, io.rm, harnessRoot);
12172
- for (const line of externalRemovalLines(external)) io.log(line);
12173
- const shared2 = stripCliShared(installLog, projectDir, harnessRoot, plan.shared, true);
12174
- for (const line of sharedStripLines(shared2, false)) io.log(line);
12175
- if (plan.importBlock && stripRootImport(projectDir)) {
12176
- io.log(` ${status.success("CLAUDE.md \u2014 harness @import removed (\uBCF8\uBB38 \uBCF4\uC874)")}`);
12586
+ const done = removeHarnessFiles(installLog, projectDir, plan.scope, {
12587
+ rm: io.rm,
12588
+ write: true,
12589
+ harnessRoot
12590
+ });
12591
+ for (const line of harnessRemovalLines(done, installLog, projectDir, plan.scope, false)) {
12592
+ io.log(line);
12177
12593
  }
12178
- const anchorRemoved = plan.anchor ? removeHarnessAnchor(ctx, io) : false;
12594
+ for (const line of assetsLeftLines(installLog, projectDir, plan.scope)) io.log(line);
12179
12595
  const rerenderSet = agentsMdRerenderSet(target, remaining, installLog, projectDir);
12180
12596
  const rerender = rerenderSet === null ? null : rerenderAgentsMd(ctx, rerenderSet);
12181
12597
  if (rerender !== null) for (const line of rerenderLines(rerender)) io.log(line);
12182
- const recovered = /* @__PURE__ */ new Set([...external.removed, ...external.stripped]);
12183
- const settled = settleCliLog(ctx, target, remaining, recovered, anchorRemoved, plan.dirs, shared2);
12598
+ const settled = settleCliLog(ctx, target, remaining, done, done.anchor.removed);
12184
12599
  const next = rerender === null ? settled : withRerender(settled, rerender);
12185
12600
  try {
12186
12601
  io.writeLog(projectDir, next);
@@ -12199,19 +12614,36 @@ function removeCliAction(ctx, io) {
12199
12614
  io.log(status.success(c.green(`${target} removed (remaining: ${remaining.join(", ")})`)));
12200
12615
  io.exit(0);
12201
12616
  }
12202
- var CLI_SHARED = ["AGENTS.md", "opencode.json"];
12203
- var ROOT_SHARED = [".mcp.json", ".gitignore"];
12617
+ var UNINSTALL_SHARED = Object.keys(SHARED_FILES).filter(
12618
+ (p2) => p2 !== "CLAUDE.md"
12619
+ );
12204
12620
  var SKILLS_LOCK = "skills-lock.json";
12205
12621
  function withSkillsLock(rootFiles, assets, projectDir) {
12206
12622
  const out = [...rootFiles];
12207
12623
  const skills = assets.some((a) => a.method === "skill" && a.scope === "project");
12208
- if (skills && !out.some((f) => f.path === SKILLS_LOCK) && existsSync25(join34(projectDir, SKILLS_LOCK))) {
12624
+ if (skills && !out.some((f) => f.path === SKILLS_LOCK) && existsSync26(join34(projectDir, SKILLS_LOCK))) {
12209
12625
  out.push({ path: SKILLS_LOCK, change: "advisory", notes: [] });
12210
12626
  }
12211
12627
  return out;
12212
12628
  }
12213
- function remnantFor(path, log, harnessRoot) {
12629
+ function remnantFor(path, log, harnessRoot, projectDir) {
12214
12630
  const clis = installedClis(log);
12631
+ if (path === SETTINGS_TARGET) {
12632
+ return (disk) => {
12633
+ try {
12634
+ const template = readFileSync26(join34(harnessRoot, "templates", "settings.json"), "utf8");
12635
+ const render = renderSettingsPortion(template, () => true);
12636
+ return [...legacySettingsSeed(disk, render, projectDir).keys()].filter(
12637
+ (k2) => !k2.endsWith("{}")
12638
+ );
12639
+ } catch {
12640
+ return [];
12641
+ }
12642
+ };
12643
+ }
12644
+ if (path === CODEX_CONFIG) {
12645
+ return (disk) => [...ADAPTERS["toml-region"].read(disk)?.keys() ?? []];
12646
+ }
12215
12647
  if (path === ".gitignore") {
12216
12648
  return (disk) => [...legacyGitignoreSeed(disk, gitignoreRender(), log).keys()];
12217
12649
  }
@@ -12261,6 +12693,12 @@ function knownServerValues(servers, form, prefix = "mcpServers.") {
12261
12693
  return out;
12262
12694
  }
12263
12695
  function remnantLine(path) {
12696
+ if (path === SETTINGS_TARGET) {
12697
+ return (names) => `may still hold the harness's hooks (not on record): ${names.join(" \xB7 ")} \u2014 delete them by hand if they are not yours`;
12698
+ }
12699
+ if (path === CODEX_CONFIG) {
12700
+ return (names) => `harness regions not on record: ${names.join(" \xB7 ")} \u2014 delete them by hand if you want them gone`;
12701
+ }
12264
12702
  if (path === ".mcp.json") {
12265
12703
  return (names) => `may still hold the harness's MCP servers (not on record): ${names.join(" \xB7 ")} \u2014 delete them by hand if they are not yours`;
12266
12704
  }
@@ -12269,31 +12707,11 @@ function remnantLine(path) {
12269
12707
  }
12270
12708
  return path === AGENTS_MD2 ? () => "the harness block is not on record (written before the harness recorded its part) \u2014 delete the <!-- uzys-harness:agents --> block by hand if you want it gone" : (names) => `may still hold the harness's MCP servers (not on record): ${names.join(" \xB7 ")} \u2014 delete them by hand if they are not yours`;
12271
12709
  }
12272
- function stripCliShared(log, projectDir, harnessRoot, paths, write) {
12273
- const harnessMade = new Set((log.externalFiles ?? []).map((f) => f.path));
12274
- const created = new Set(
12275
- (log.rootFiles ?? []).filter((f) => f.change === "created").map((f) => f.path)
12276
- );
12277
- return paths.filter((path) => !harnessMade.has(path)).map(
12278
- (path) => stripShared({
12279
- projectDir,
12280
- path,
12281
- portions: log.portions ?? [],
12282
- excluded: [...excludedIds(log)],
12283
- remnant: remnantFor(path, log, harnessRoot),
12284
- remnantLine: remnantLine(path),
12285
- write,
12286
- // ADR-099 R2 — 블록만 담아 다시 만든 `AGENTS.md`(첫 접촉 블록 모델)도 하네스가 만든 파일이다
12287
- removeIfEmpty: created.has(path)
12288
- })
12289
- );
12290
- }
12291
12710
  function sharedStripLines(results, preview) {
12292
12711
  const lines2 = [];
12293
12712
  for (const r of results) {
12294
12713
  const display = (k2) => {
12295
12714
  if (k2 === AGENTS_BLOCK_NAME) return ["harness block"];
12296
- if (!ROOT_SHARED.includes(r.path)) return [k2];
12297
12715
  const id = keyId(r.path, k2);
12298
12716
  return id === null ? [] : [id.slice(id.indexOf(":") + 1)];
12299
12717
  };
@@ -12343,7 +12761,7 @@ function noReverseHints(asset, ctx) {
12343
12761
  }
12344
12762
  if (asset.method !== "skill") return [];
12345
12763
  const name = asset.detail.skill;
12346
- const left = name === void 0 ? [] : [".claude/skills", ".agents/skills"].map((root) => `${root}/${name}/`).filter((p2) => !underAny(p2, ctx.movedDirs) && existsSync25(join34(ctx.projectDir, p2)));
12764
+ const left = name === void 0 ? [] : [".claude/skills", ".agents/skills"].map((root) => `${root}/${name}/`).filter((p2) => existsSync26(join34(ctx.projectDir, p2)));
12347
12765
  return [
12348
12766
  left.length > 0 ? `files it put down are not on record (installed by an older version) \u2014 left: ${left.join(" \xB7 ")} (delete by hand if the harness installed it)` : "files it put down are not on record (installed by an older version) \u2014 nothing removed"
12349
12767
  ];
@@ -12423,118 +12841,513 @@ function buildGlobalAdvisoryCmd(asset) {
12423
12841
  return "(no standard reverse \u2014 manual)";
12424
12842
  }
12425
12843
  }
12426
- function removeTemplates(log, projectDir, io, harnessRoot) {
12427
- const { rm } = io;
12428
- const movedDirs = removedDirsList(log);
12429
- const recovered = recoverMovedSymlinkTargets(log, projectDir, movedDirs, io.rm);
12430
- const outsidePortions = recoverMovedSymlinkPortions(log, projectDir, movedDirs);
12431
- const keptLinks = [];
12432
- const moved = recordedTemplateDirs(log).flatMap((rel) => {
12433
- const kept = outsideTemplateLink(projectDir, rel);
12434
- if (kept) {
12435
- keptLinks.push(kept);
12436
- return [];
12844
+ var CLAUDE_DIR3 = ".claude/";
12845
+ var LEGACY_LOG = `${LEGACY_INSTALL_LOG_DIR}/${INSTALL_LOG_FILENAME}`;
12846
+ var SKILLS_DIR3 = ".claude/skills/";
12847
+ var CLI_FOLDERS2 = [".claude", ".codex", ".opencode"];
12848
+ var LEFT_FILES_SHOWN = 3;
12849
+ var FULL_SCOPE = {
12850
+ has: () => true,
12851
+ shared: UNINSTALL_SHARED,
12852
+ skeleton: PROJECT_SKELETON_DIRS,
12853
+ folders: CLI_FOLDERS2,
12854
+ importBlock: true,
12855
+ anchor: true
12856
+ };
12857
+ function removeHarnessFiles(log, projectDir, scope, io) {
12858
+ const out = {
12859
+ removed: [],
12860
+ backedUp: [],
12861
+ forgotten: /* @__PURE__ */ new Set(),
12862
+ stripped: [],
12863
+ kept: [],
12864
+ unjudged: [],
12865
+ outside: [],
12866
+ displaced: [],
12867
+ shared: [],
12868
+ retiredWiring: [],
12869
+ importStripped: false,
12870
+ anchor: { removed: false, lines: [] }
12871
+ };
12872
+ const pruneFrom = [];
12873
+ stripSharedFiles(log, projectDir, scope.shared, io, out, pruneFrom);
12874
+ const isTarget = harnessTargets(log);
12875
+ for (const path of harnessCandidates(log)) {
12876
+ if (scope.has(path))
12877
+ removeRecordedFile(path, { log, projectDir, io, out, isTarget, pruneFrom });
12878
+ }
12879
+ if (scope.importBlock) {
12880
+ out.importStripped = io.write ? stripRootImport(projectDir) : hasRootImport(projectDir);
12881
+ }
12882
+ if (scope.anchor) {
12883
+ out.anchor = removeAnchor(log, projectDir, io);
12884
+ const anchorPath = log.templates?.rootClaudeMd?.path;
12885
+ if (out.anchor.removed && anchorPath !== void 0) out.removedAnchor = anchorPath;
12886
+ }
12887
+ restoreDisplaced(log, projectDir, scope, io, out);
12888
+ if (io.write) {
12889
+ for (const p2 of pruneFrom) pruneEmptyDirsUpward(projectDir, dirname7(join34(projectDir, p2)));
12890
+ for (const d2 of scope.skeleton) {
12891
+ const abs = join34(projectDir, d2);
12892
+ if (outsideProjectTarget(projectDir, abs) !== null) continue;
12893
+ pruneEmptyDirsUpward(projectDir, abs);
12437
12894
  }
12438
- const backup = io.moveAside(templateDirPath(projectDir, rel));
12439
- return backup ? [{ rel, backup }] : [];
12440
- });
12441
- const external = removeExternalFiles(log, projectDir, rm, harnessRoot);
12442
- external.removed.push(...recovered.removed);
12443
- external.kept.push(...recovered.kept);
12444
- for (const o of [...recovered.outside, ...outsidePortions]) {
12445
- if (!external.outside.some((e2) => e2.path === o.path)) external.outside.push(o);
12446
- }
12447
- external.outside = external.outside.filter((o) => !underKeptLink(o.path, keptLinks));
12448
- const importStripped = stripRootImport(projectDir);
12449
- const rootMd = log.templates?.rootClaudeMd;
12450
- if (rootMd) {
12451
- if (rootClaudeMdModified(log, projectDir))
12452
- return { rootClaudeMdKept: true, importStripped, external, moved, keptLinks };
12453
- rm(join34(projectDir, rootMd.path));
12454
12895
  }
12455
- return { rootClaudeMdKept: false, importStripped, external, moved, keptLinks };
12896
+ return out;
12456
12897
  }
12457
- var AGENTS_MD2 = "AGENTS.md";
12458
- function readAgentsMdTemplate(harnessRoot) {
12898
+ function harnessCandidates(log) {
12899
+ const paths = [
12900
+ ...(log.policyFiles ?? []).map((f) => `${CLAUDE_DIR3}${f.path}`),
12901
+ ...(log.skillFiles ?? []).map((f) => `${SKILLS_DIR3}${f.path}`),
12902
+ ...(log.externalFiles ?? []).map((f) => f.path).filter((p2) => !p2.startsWith(`${INSTALL_LOG_DIR}/`)),
12903
+ // 어느 필드에도 적힌 적 없는 옛 판의 하네스 파일 — `isTarget` 이 알려 준다(A8 · install 과 같은 방식)
12904
+ INSTALLED_TRACKS,
12905
+ // 스캔 필드가 아예 없는 옛 로그(v26.125.0 이하)는 필드가 하네스 파일을 열거하지 못한다 — manifest 의 하네스 자리 ·
12906
+ // 은퇴 경로를 후보로 넣고 `recorded()` 가 가른다(필드 없음 → `no-sha` · 필드 있음 → 기록에 없으면 `none`, PR-D 리뷰 N1).
12907
+ // 그 은퇴 자리는 새 install · update 가 필드를 쓸 때 `policyFiles` 로 이어받는다(`retiredClaims` — 재리뷰 BLOCKER-1 ②)
12908
+ ...log.records === "writer" ? [] : recordedTrackSpots(log)
12909
+ ];
12910
+ return [...new Set(paths)].filter((p2) => kindOf(log, p2) === "harness");
12911
+ }
12912
+ function harnessTargets(log) {
12913
+ const tracked = recordedTrackTargets(log);
12914
+ return (p2) => p2 === INSTALLED_TRACKS || tracked(p2);
12915
+ }
12916
+ function templateNext(harnessRoot, log, path) {
12917
+ if (path === INSTALLED_TRACKS) return installedTracksText(log.spec.tracks.filter(isTrack));
12918
+ if (!path.startsWith(CLAUDE_DIR3)) return null;
12459
12919
  try {
12460
- return readFileSync26(join34(harnessRoot, AGENTS_MD_TEMPLATE), "utf8");
12920
+ return readFileSync26(join34(harnessRoot, "templates", path.slice(CLAUDE_DIR3.length)), "utf8");
12461
12921
  } catch {
12462
12922
  return null;
12463
12923
  }
12464
12924
  }
12465
- function recoverMovedSymlinkPortions(log, projectDir, dirs) {
12466
- const outside = [];
12467
- const paths = new Set((log.portions ?? []).map((p2) => p2.path));
12925
+ function removeRecordedFile(path, ctx) {
12926
+ const { log, projectDir, io, out } = ctx;
12927
+ const abs = join34(projectDir, path);
12928
+ let foreign;
12929
+ try {
12930
+ foreign = foreignOwnedTarget(projectDir, path);
12931
+ } catch {
12932
+ out.unjudged.push(path);
12933
+ return;
12934
+ }
12935
+ if (foreign !== null) {
12936
+ out.forgotten.add(path);
12937
+ return;
12938
+ }
12939
+ const outside = outsideProjectTarget(projectDir, abs);
12940
+ if (outside !== null) {
12941
+ out.outside.push(outside);
12942
+ return;
12943
+ }
12944
+ let st2;
12945
+ try {
12946
+ st2 = lstatSync5(abs, { throwIfNoEntry: false });
12947
+ } catch {
12948
+ out.unjudged.push(path);
12949
+ return;
12950
+ }
12951
+ if (st2 === void 0 || st2.isSymbolicLink() && !existsSync26(abs)) {
12952
+ out.forgotten.add(path);
12953
+ return;
12954
+ }
12955
+ let effective = abs;
12956
+ let disk;
12957
+ try {
12958
+ const real = realpathSync3(abs);
12959
+ if (!statSync5(real).isFile()) {
12960
+ out.unjudged.push(path);
12961
+ return;
12962
+ }
12963
+ if (st2.isSymbolicLink()) effective = real;
12964
+ disk = readFileSync26(effective, "utf8");
12965
+ } catch {
12966
+ out.unjudged.push(path);
12967
+ return;
12968
+ }
12969
+ const verdict = judge({
12970
+ op: "remove",
12971
+ kind: "harness",
12972
+ rec: recorded(log, path, { isTarget: ctx.isTarget }),
12973
+ disk,
12974
+ next: templateNext(io.harnessRoot, log, path)
12975
+ });
12976
+ if (verdict.verdict !== "remove" && verdict.verdict !== "backup+remove") return;
12977
+ let backup = null;
12978
+ if (verdict.verdict === "backup+remove") {
12979
+ backup = io.write ? relative5(projectDir, backupFile(effective)).split(sep5).join("/") : `${relative5(projectDir, effective).split(sep5).join("/")}.backup-<time>`;
12980
+ }
12981
+ if (io.write) {
12982
+ if (effective !== abs) io.rm(abs);
12983
+ io.rm(effective);
12984
+ }
12985
+ out.removed.push(path);
12986
+ out.forgotten.add(path);
12987
+ ctx.pruneFrom.push(path);
12988
+ if (backup !== null) out.backedUp.push({ path, backup, line: verdict.line });
12989
+ }
12990
+ function stripSharedFiles(log, projectDir, paths, io, out, pruneFrom) {
12991
+ const made = new Map((log.externalFiles ?? []).map((f) => [f.path, f.sha256]));
12992
+ const created = new Set(
12993
+ (log.rootFiles ?? []).filter((f) => f.change === "created").map((f) => f.path)
12994
+ );
12995
+ const onRecord = /* @__PURE__ */ new Set([
12996
+ ...(log.portions ?? []).map((p2) => p2.path),
12997
+ ...made.keys(),
12998
+ ...(log.rootFiles ?? []).map((f) => f.path)
12999
+ ]);
13000
+ const clis = installedClis(log);
13001
+ const relevant = (path) => {
13002
+ const owners = ownersOf(path);
13003
+ return onRecord.has(path) || owners.length === 0 || owners.some((c3) => clis.includes(c3));
13004
+ };
12468
13005
  for (const path of paths) {
12469
- if (path.startsWith(".agents/skills/")) continue;
12470
- if (!dirs.some((d2) => path.startsWith(d2))) continue;
13006
+ if (!relevant(path)) continue;
12471
13007
  const abs = join34(projectDir, path);
12472
- if (!safeIsSymlink(abs)) continue;
12473
- const target = safeRealpath(abs);
12474
- if (target !== null && isOutsideProject(projectDir, target)) {
12475
- outside.push({ path, target });
13008
+ if (safeExists(abs)) {
13009
+ const o = outsideProjectTarget(projectDir, abs);
13010
+ if (o !== null) {
13011
+ out.outside.push(o);
13012
+ continue;
13013
+ }
13014
+ }
13015
+ if (path === SETTINGS_TARGET && safeExists(abs)) {
13016
+ out.retiredWiring = removeRetiredHookRefs(
13017
+ abs,
13018
+ join34(projectDir, ".claude"),
13019
+ RETIRED_HOOK_WIRING_REFS,
13020
+ { evenIfScriptExists: true, write: io.write }
13021
+ );
13022
+ }
13023
+ const sha = made.get(path);
13024
+ if (sha !== void 0 && removeHarnessMadeShared(path, sha, projectDir, io, out, pruneFrom)) {
12476
13025
  continue;
12477
13026
  }
12478
- stripShared({
13027
+ const r = stripShared({
12479
13028
  projectDir,
12480
13029
  path,
12481
13030
  portions: log.portions ?? [],
12482
13031
  excluded: [...excludedIds(log)],
12483
- remnant: () => [],
12484
- remnantLine: () => "",
12485
- write: true
13032
+ remnant: remnantFor(path, log, io.harnessRoot, projectDir),
13033
+ remnantLine: remnantLine(path),
13034
+ write: io.write,
13035
+ // 하네스가 만든 파일(#569 `created` · #13 `externalFiles`)은 몫을 걷고 남는 것이 없으면 파일째 지운다
13036
+ removeIfEmpty: created.has(path) || sha !== void 0
12486
13037
  });
13038
+ if (r.fileRemoved) pruneFrom.push(path);
13039
+ out.shared.push(r);
12487
13040
  }
12488
- return outside;
12489
13041
  }
12490
- function removedDirsList(log) {
12491
- return recordedTemplateDirs(log);
13042
+ function removeHarnessMadeShared(path, sha, projectDir, io, out, pruneFrom) {
13043
+ const abs = join34(projectDir, path);
13044
+ if (!existsSync26(abs)) {
13045
+ out.forgotten.add(path);
13046
+ return true;
13047
+ }
13048
+ let effective = abs;
13049
+ let current;
13050
+ try {
13051
+ const real = realpathSync3(abs);
13052
+ if (!statSync5(real).isFile()) {
13053
+ out.unjudged.push(path);
13054
+ return true;
13055
+ }
13056
+ if (lstatSync5(abs).isSymbolicLink()) effective = real;
13057
+ current = readFileSync26(effective, "utf8");
13058
+ } catch {
13059
+ out.unjudged.push(path);
13060
+ return true;
13061
+ }
13062
+ if (hashContent(current) !== sha) {
13063
+ if (path === AGENTS_MD2) {
13064
+ out.kept.push(path);
13065
+ return true;
13066
+ }
13067
+ out.forgotten.add(path);
13068
+ return false;
13069
+ }
13070
+ if (path === AGENTS_MD2) {
13071
+ const verdict = agentsMdRemainder(current, io.harnessRoot);
13072
+ if (!verdict.ok) {
13073
+ out.unjudged.push(path);
13074
+ return true;
13075
+ }
13076
+ if (verdict.remainder !== null) {
13077
+ try {
13078
+ if (io.write) writeFileSync10(effective, neutralizeScaffoldBanner(verdict.remainder), "utf8");
13079
+ out.stripped.push(path);
13080
+ out.forgotten.add(path);
13081
+ } catch {
13082
+ out.unjudged.push(path);
13083
+ }
13084
+ return true;
13085
+ }
13086
+ }
13087
+ if (io.write) {
13088
+ if (effective !== abs) io.rm(abs);
13089
+ io.rm(effective);
13090
+ }
13091
+ out.shared.push({
13092
+ path,
13093
+ action: "removed",
13094
+ line: "",
13095
+ removed: [],
13096
+ kept: [],
13097
+ portions: [],
13098
+ fileRemoved: true
13099
+ });
13100
+ out.forgotten.add(path);
13101
+ pruneFrom.push(path);
13102
+ return true;
12492
13103
  }
12493
- function recoverMovedSymlinkTargets(log, projectDir, dirs, rm) {
12494
- const removed = [];
12495
- const kept = [];
12496
- const outside = [];
12497
- for (const { path, sha256 } of log.externalFiles ?? []) {
12498
- if (path.startsWith(".agents/skills/")) continue;
12499
- if (!dirs.some((d2) => path.startsWith(d2))) continue;
12500
- const abs = join34(projectDir, path);
12501
- if (!safeIsSymlink(abs)) continue;
12502
- let target;
12503
- try {
12504
- target = realpathSync3(abs);
12505
- if (!statSync4(target).isFile()) continue;
12506
- } catch {
12507
- continue;
13104
+ function restoreDisplaced(log, projectDir, scope, io, out) {
13105
+ const willBeGone = /* @__PURE__ */ new Set([
13106
+ ...out.removed,
13107
+ ...out.removedAnchor === void 0 ? [] : [out.removedAnchor]
13108
+ ]);
13109
+ for (const f of log.rootFiles ?? []) {
13110
+ if (f.change !== "displaced" || !scope.has(f.path)) continue;
13111
+ const slot = join34(projectDir, f.path);
13112
+ if (outsideProjectTarget(projectDir, slot) !== null) continue;
13113
+ const backup = f.notes[0] ?? null;
13114
+ const d2 = judgeDisplaced({
13115
+ backup,
13116
+ backupExists: backup !== null && safeExists(join34(projectDir, backup)),
13117
+ slotEmpty: !safeExists(slot) || !io.write && willBeGone.has(f.path)
13118
+ });
13119
+ if (io.write && d2.verdict === "restore" && backup !== null) {
13120
+ mkdirSync7(dirname7(slot), { recursive: true });
13121
+ renameSync(join34(projectDir, backup), slot);
12508
13122
  }
12509
- if (isOutsideProject(projectDir, target)) {
12510
- outside.push({ path, target });
12511
- continue;
13123
+ out.displaced.push({
13124
+ path: f.path,
13125
+ backup,
13126
+ verdict: d2.verdict,
13127
+ line: d2.line,
13128
+ forget: d2.record === "forget"
13129
+ });
13130
+ }
13131
+ }
13132
+ function topOf(path) {
13133
+ const i = path.indexOf("/");
13134
+ return i < 0 ? path : `${path.slice(0, i)}/`;
13135
+ }
13136
+ function harnessRemovalLines(r, log, projectDir, scope, preview) {
13137
+ const lines2 = [];
13138
+ const groups = /* @__PURE__ */ new Map();
13139
+ for (const p2 of r.removed) {
13140
+ const g = groups.get(topOf(p2)) ?? { removed: 0, backedUp: 0 };
13141
+ g.removed += 1;
13142
+ groups.set(topOf(p2), g);
13143
+ }
13144
+ for (const b2 of r.backedUp) {
13145
+ const g = groups.get(topOf(b2.path));
13146
+ if (g) g.backedUp += 1;
13147
+ }
13148
+ if (preview) {
13149
+ for (const [top, g] of groups) {
13150
+ const edited = g.backedUp > 0 ? ` (${g.backedUp} you edited \u2192 saved beside as *.backup-<time>)` : "";
13151
+ lines2.push(` \u25CB remove ${g.removed} harness file(s) under ${top}${edited}`);
13152
+ }
13153
+ } else if (groups.size > 0) {
13154
+ const parts = [...groups].map(
13155
+ ([top, g], i) => i === 0 ? `${top} ${g.removed} harness file(s)` : `${top} ${g.removed}`
13156
+ );
13157
+ lines2.push(` ${status.success(`removed ${parts.join(" \xB7 ")}`)}`);
13158
+ for (const b2 of r.backedUp) {
13159
+ const what = b2.line.includes("<file>.backup-<time>") ? b2.line.replace("<file>.backup-<time>", b2.backup) : `${b2.line} (${b2.backup})`;
13160
+ lines2.push(` ${status.success(`backed up ${b2.path} \u2014 ${what}`)}`);
12512
13161
  }
13162
+ }
13163
+ if (r.importStripped) {
13164
+ lines2.push(
13165
+ preview ? " \u25CB strip harness @import from CLAUDE.md (\uBCF8\uBB38 \uBCF4\uC874)" : ` ${status.success("CLAUDE.md \u2014 harness @import removed (\uBCF8\uBB38 \uBCF4\uC874)")}`
13166
+ );
13167
+ }
13168
+ lines2.push(...r.anchor.lines);
13169
+ const restore = r.displaced.filter((d2) => d2.verdict === "restore");
13170
+ if (preview && restore.length > 0) {
13171
+ lines2.push(
13172
+ ` \u25CB put back ${restore.length} file(s) of yours (displaced): ${restore.map((d2) => d2.path).join(" \xB7 ")}`
13173
+ );
13174
+ }
13175
+ for (const d2 of r.displaced) {
13176
+ if (d2.verdict === "restore" && !preview) {
13177
+ lines2.push(` ${status.success(`put back ${d2.path} (from ${d2.backup})`)}`);
13178
+ } else if (d2.verdict === "advise") {
13179
+ lines2.push(` ${c.yellow("\u2298")} ${d2.path} \u2014 ${d2.line}`);
13180
+ }
13181
+ }
13182
+ for (const path of r.stripped) {
13183
+ lines2.push(
13184
+ preview ? ` \u25CB strip harness sections from ${path} (\uBCF8\uBB38 \uBCF4\uC874)` : ` ${status.success(`${path} \u2014 harness sections removed (\uBCF8\uBB38 \uBCF4\uC874)`)}`
13185
+ );
13186
+ }
13187
+ for (const path of r.unjudged) {
13188
+ lines2.push(
13189
+ path === AGENTS_MD2 ? ` ${c.yellow("\u2298")} ${path} kept \u2014 harness sections could not be separated (a file or template could not be read, or the write failed). Remove manually if intended.` : ` ${c.yellow("\u2298")} ${path} left \u2014 could not read it to judge. Remove manually if intended.`
13190
+ );
13191
+ }
13192
+ lines2.push(...outsideLines(r.outside, log));
13193
+ for (const path of r.kept) {
13194
+ lines2.push(
13195
+ ` ${c.yellow("\u2298")} ${path} kept \u2014 modified since install. Remove manually if intended.`
13196
+ );
13197
+ }
13198
+ if (r.retiredWiring.length > 0) {
13199
+ const refs = r.retiredWiring.join(" \xB7 ");
13200
+ lines2.push(
13201
+ preview ? ` \u25CB remove retired harness hook wiring from ${SETTINGS_TARGET}: ${refs}` : ` ${status.success(`${SETTINGS_TARGET} \u2014 removed retired harness hook wiring: ${refs}`)}`
13202
+ );
13203
+ }
13204
+ lines2.push(...sharedStripLines(r.shared, preview));
13205
+ lines2.push(...leftFolderLines(r, log, projectDir, scope, preview));
13206
+ return lines2;
13207
+ }
13208
+ function outsideLines(outside, log) {
13209
+ const byLink = /* @__PURE__ */ new Map();
13210
+ for (const o of outside) if (!byLink.has(o.link)) byLink.set(o.link, o);
13211
+ return [...byLink.values()].map(
13212
+ (o) => o.link === o.path ? ` ${c.yellow("\u2298")} ${o.path} kept \u2014 link target is outside the project (${o.target}). Not touched; remove manually if intended.` : keptOutsideLinkLine(o, log)
13213
+ );
13214
+ }
13215
+ function leftFolderLines(r, log, projectDir, scope, preview) {
13216
+ const assetFiles = new Set(log.assets.flatMap((a) => (a.files ?? []).map((f) => f.path)));
13217
+ const gone = new Set(
13218
+ preview ? [...r.removed, ...r.shared.filter((s) => s.fileRemoved).map((s) => s.path)] : []
13219
+ );
13220
+ const back = preview ? r.displaced.filter((d2) => d2.verdict === "restore").map((d2) => d2.path) : [];
13221
+ const said = (full) => r.unjudged.includes(full) || r.kept.includes(full) || r.outside.some((o) => o.path === full || full.startsWith(`${o.link}/`));
13222
+ const whose = "not on the install record \u2014 yours, or left by an older harness version; delete by hand any that are not yours";
13223
+ const lines2 = [];
13224
+ for (const dir of scope.folders) {
13225
+ if (outsideLinkAt(projectDir, dir) !== null) continue;
13226
+ const entries = /* @__PURE__ */ new Map();
13227
+ for (const n of yoursUnder(projectDir, dir)) {
13228
+ const full = `${dir}/${n.name}`;
13229
+ if (full === LEGACY_LOG) continue;
13230
+ if (!gone.has(full) && !assetFiles.has(full) && !said(full)) entries.set(n.name, n.shown);
13231
+ }
13232
+ for (const p2 of back)
13233
+ if (p2.startsWith(`${dir}/`)) entries.set(p2.slice(dir.length + 1), p2.slice(dir.length + 1));
13234
+ const shown = [...entries.values()].sort();
13235
+ if (shown.length === 0) continue;
13236
+ const head = shown.slice(0, LEFT_FILES_SHOWN).join(" \xB7 ");
13237
+ const more = shown.length > LEFT_FILES_SHOWN ? ` +${shown.length - LEFT_FILES_SHOWN} more` : "";
13238
+ const what = `${dir}/ \u2014 ${shown.length} file(s) ${whose}: ${head}${more}`;
13239
+ lines2.push(preview ? ` \u25CB leave ${what}` : ` ${c.yellow("\u2298")} left ${what}`);
13240
+ }
13241
+ return lines2;
13242
+ }
13243
+ function yoursUnder(projectDir, dir) {
13244
+ const out = [];
13245
+ const walk2 = (abs2, prefix) => {
13246
+ let entries;
12513
13247
  try {
12514
- if (hashContent(readFileSync26(target, "utf8")) !== sha256) {
12515
- kept.push(path);
12516
- continue;
12517
- }
12518
- rm(target);
12519
- removed.push(path);
13248
+ entries = readdirSync6(abs2, { withFileTypes: true });
12520
13249
  } catch {
12521
- kept.push(path);
13250
+ return;
12522
13251
  }
13252
+ for (const e2 of entries) {
13253
+ const name = `${prefix}${e2.name}`;
13254
+ if (e2.isDirectory()) walk2(join34(abs2, e2.name), `${name}/`);
13255
+ else if (BACKUP_SUFFIX.test(e2.name)) continue;
13256
+ else if (e2.isSymbolicLink()) {
13257
+ let target = "?";
13258
+ try {
13259
+ target = readlinkSync(join34(abs2, e2.name));
13260
+ } catch {
13261
+ }
13262
+ out.push({ name, shown: `${name} (your link \u2192 ${target})` });
13263
+ } else out.push({ name, shown: name });
13264
+ }
13265
+ };
13266
+ const abs = join34(projectDir, dir);
13267
+ try {
13268
+ if (!statSync5(abs).isDirectory()) return [];
13269
+ } catch {
13270
+ return [];
12523
13271
  }
12524
- return { removed, kept, outside };
13272
+ walk2(abs, "");
13273
+ return out;
12525
13274
  }
12526
- function safeRealpath(abs) {
13275
+ function assetsLeftLines(log, projectDir, scope) {
13276
+ const lines2 = [];
13277
+ for (const a of log.assets) {
13278
+ const here = (a.files ?? []).map((f) => f.path).filter((p2) => scope.has(p2) && safeExists(join34(projectDir, p2)));
13279
+ if (here.length === 0) continue;
13280
+ const slots = [...new Set(here.map((p2) => p2.split("/").slice(0, 3).join("/")))].sort();
13281
+ lines2.push(
13282
+ ` ${c.yellow("\u2298")} left ${slots.join(" \xB7 ")} (asset ${a.id}) \u2014 remove with: agent-harness uninstall --only ${a.id}`
13283
+ );
13284
+ }
13285
+ return lines2;
13286
+ }
13287
+ function removeAnchor(log, projectDir, io) {
13288
+ const rootMd = log.templates?.rootClaudeMd;
13289
+ if (!rootMd) return { removed: false, lines: [] };
13290
+ const abs = join34(projectDir, rootMd.path);
13291
+ if (!existsSync26(abs)) return { removed: false, lines: [] };
13292
+ const outside = outsideProjectTarget(projectDir, abs);
13293
+ if (outside !== null) return { removed: false, lines: outsideLines([outside], log) };
13294
+ let disk;
12527
13295
  try {
12528
- return realpathSync3(abs);
13296
+ disk = readFileSync26(abs, "utf8");
13297
+ } catch {
13298
+ return {
13299
+ removed: false,
13300
+ lines: [
13301
+ ` ${c.yellow("\u2298")} ${rootMd.path} left \u2014 could not read it to judge. Remove manually if intended.`
13302
+ ]
13303
+ };
13304
+ }
13305
+ const verdict = judge({
13306
+ op: "remove",
13307
+ kind: "harness",
13308
+ rec: { state: "sha", sha256: rootMd.sha256 },
13309
+ disk,
13310
+ next: null
13311
+ });
13312
+ const edited = verdict.verdict === "backup+remove";
13313
+ if (edited && rootMd.path === "CLAUDE.md") {
13314
+ return {
13315
+ removed: false,
13316
+ lines: [
13317
+ io.write ? ` ${c.yellow("\u2298")} ${rootMd.path} kept \u2014 modified since install. Remove manually if intended.` : ` \u25CB keep ${rootMd.path} (modified since install \u2014 preserved)`
13318
+ ]
13319
+ };
13320
+ }
13321
+ if (!io.write) {
13322
+ return {
13323
+ removed: true,
13324
+ lines: [
13325
+ edited ? ` \u25CB remove ${rootMd.path} (you edited it \u2192 saved beside as ${rootMd.path}.backup-<time>)` : ` \u25CB remove ${rootMd.path}`
13326
+ ]
13327
+ };
13328
+ }
13329
+ const backup = edited ? relative5(projectDir, backupFile(abs)).split(sep5).join("/") : null;
13330
+ io.rm(abs);
13331
+ return {
13332
+ removed: true,
13333
+ lines: [
13334
+ backup === null ? ` ${status.success(`${rootMd.path} removed`)}` : ` ${status.success(`backed up ${rootMd.path} \u2014 ${verdict.line.replace("<file>.backup-<time>", backup)}`)}`
13335
+ ]
13336
+ };
13337
+ }
13338
+ var AGENTS_MD2 = "AGENTS.md";
13339
+ function readAgentsMdTemplate(harnessRoot) {
13340
+ try {
13341
+ return readFileSync26(join34(harnessRoot, AGENTS_MD_TEMPLATE), "utf8");
12529
13342
  } catch {
12530
13343
  return null;
12531
13344
  }
12532
13345
  }
12533
- function safeIsSymlink(abs) {
13346
+ function safeRealpath(abs) {
12534
13347
  try {
12535
- return lstatSync5(abs).isSymbolicLink();
13348
+ return realpathSync3(abs);
12536
13349
  } catch {
12537
- return false;
13350
+ return null;
12538
13351
  }
12539
13352
  }
12540
13353
  function neutralizeScaffoldBanner(text) {
@@ -12552,71 +13365,6 @@ function agentsMdRemainder(current, harnessRoot) {
12552
13365
  })
12553
13366
  };
12554
13367
  }
12555
- function removeExternalFiles(log, projectDir, rm, harnessRoot) {
12556
- const removed = [];
12557
- const kept = [];
12558
- const stripped = [];
12559
- const unjudged = [];
12560
- const outside = [];
12561
- for (const { path, sha256 } of log.externalFiles ?? []) {
12562
- const abs = join34(projectDir, path);
12563
- if (!existsSync25(abs)) continue;
12564
- let current;
12565
- let effective = abs;
12566
- const foreignSkillSlot = path.startsWith(".agents/skills/");
12567
- try {
12568
- const isLink = lstatSync5(abs).isSymbolicLink();
12569
- if (isLink && foreignSkillSlot) continue;
12570
- const resolved = realpathSync3(abs);
12571
- if (!statSync4(resolved).isFile()) continue;
12572
- if (isOutsideProject(projectDir, resolved)) {
12573
- outside.push({ path, target: resolved });
12574
- continue;
12575
- }
12576
- if (isLink) effective = resolved;
12577
- current = readFileSync26(effective, "utf8");
12578
- } catch {
12579
- unjudged.push(path);
12580
- continue;
12581
- }
12582
- if (hashContent(current) !== sha256) {
12583
- kept.push(path);
12584
- continue;
12585
- }
12586
- if (path === AGENTS_MD2) {
12587
- const verdict = agentsMdRemainder(current, harnessRoot);
12588
- if (!verdict.ok) {
12589
- unjudged.push(path);
12590
- continue;
12591
- }
12592
- if (verdict.remainder !== null) {
12593
- try {
12594
- writeFileSync9(effective, neutralizeScaffoldBanner(verdict.remainder), "utf8");
12595
- stripped.push(path);
12596
- } catch {
12597
- unjudged.push(path);
12598
- }
12599
- continue;
12600
- }
12601
- }
12602
- rm(effective);
12603
- if (effective !== abs) {
12604
- try {
12605
- rm(abs);
12606
- } catch {
12607
- }
12608
- }
12609
- removed.push(path);
12610
- }
12611
- for (const path of removed) pruneEmptyDirsUpward(projectDir, dirname6(join34(projectDir, path)));
12612
- const decisions = join34(projectDir, "docs", "decisions");
12613
- try {
12614
- if (existsSync25(decisions) && readdirSync6(decisions).length === 0)
12615
- rmSync5(decisions, { recursive: true });
12616
- } catch {
12617
- }
12618
- return { removed, kept, stripped, unjudged, outside };
12619
- }
12620
13368
  function removeToolFiles(files, projectDir, rm) {
12621
13369
  const notes = [];
12622
13370
  const removed = [];
@@ -12650,7 +13398,7 @@ function removeToolFiles(files, projectDir, rm) {
12650
13398
  notes.push(`left ${path} \u2014 ${e2 instanceof Error ? e2.message : String(e2)}`);
12651
13399
  }
12652
13400
  }
12653
- for (const path of removed) pruneEmptyDirsUpward(projectDir, dirname6(join34(projectDir, path)));
13401
+ for (const path of removed) pruneEmptyDirsUpward(projectDir, dirname7(join34(projectDir, path)));
12654
13402
  return failed > 0 ? { ok: false, message: `${failed} file(s) could not be removed`, notes } : { ok: true, notes };
12655
13403
  }
12656
13404
  function pruneEmptyDirsUpward(projectDir, startDir) {
@@ -12664,7 +13412,7 @@ function pruneEmptyDirsUpward(projectDir, startDir) {
12664
13412
  } catch {
12665
13413
  return;
12666
13414
  }
12667
- cursor = dirname6(cursor);
13415
+ cursor = dirname7(cursor);
12668
13416
  }
12669
13417
  }
12670
13418
  function stripRootImport(projectDir) {
@@ -12673,121 +13421,31 @@ function stripRootImport(projectDir) {
12673
13421
  if (stripped === null) return false;
12674
13422
  try {
12675
13423
  backupIfLossyUtf8(join34(projectDir, "CLAUDE.md"));
12676
- writeFileSync9(join34(projectDir, "CLAUDE.md"), stripped, "utf8");
13424
+ writeFileSync10(join34(projectDir, "CLAUDE.md"), stripped, "utf8");
12677
13425
  return true;
12678
13426
  } catch {
12679
13427
  return false;
12680
13428
  }
12681
13429
  }
12682
- function previewExternalLines(installLog, projectDir, harnessRoot, movedDirs = []) {
12683
- const lines2 = [];
12684
- let removable = 0;
12685
- for (const { path, sha256 } of installLog.externalFiles ?? []) {
12686
- const abs = join34(projectDir, path);
12687
- const underMoved = underAny(path, movedDirs);
12688
- const isLink = !underMoved || existsSync25(abs) ? safeIsSymlink(abs) : false;
12689
- if (underMoved && !isLink) continue;
12690
- if (!existsSync25(abs)) continue;
12691
- let effective = abs;
12692
- const foreignSkillSlot = path.startsWith(".agents/skills/");
12693
- try {
12694
- const isLinkPath = lstatSync5(abs).isSymbolicLink();
12695
- if (isLinkPath && foreignSkillSlot) continue;
12696
- const resolved = realpathSync3(abs);
12697
- if (!statSync4(resolved).isFile()) continue;
12698
- if (isOutsideProject(projectDir, resolved)) {
12699
- lines2.push(` \u25CB keep ${path} (link target outside project: ${resolved} \u2014 preserved)`);
12700
- continue;
12701
- }
12702
- if (isLinkPath) effective = resolved;
12703
- } catch {
12704
- continue;
12705
- }
12706
- const current = readFileSync26(effective, "utf8");
12707
- if (hashContent(current) !== sha256) {
12708
- lines2.push(` \u25CB keep ${path} (modified since install \u2014 preserved)`);
12709
- continue;
12710
- }
12711
- if (path === AGENTS_MD2) {
12712
- const verdict = agentsMdRemainder(current, harnessRoot);
12713
- if (!verdict.ok) {
12714
- lines2.push(` \u25CB keep ${path} (harness sections could not be separated \u2014 preserved)`);
12715
- continue;
12716
- }
12717
- if (verdict.remainder !== null) {
12718
- lines2.push(` \u25CB strip harness sections from ${path} (\uBCF8\uBB38 \uBCF4\uC874)`);
12719
- continue;
12720
- }
12721
- }
12722
- removable += 1;
12723
- }
12724
- if (removable > 0) lines2.unshift(` \u25CB remove ${removable} CLI output file(s)`);
12725
- return lines2;
12726
- }
12727
- function externalRemovalLines(external) {
12728
- const lines2 = [];
12729
- if (external.removed.length > 0) {
12730
- lines2.push(` ${status.success(`CLI outputs removed: ${external.removed.length} file(s)`)}`);
12731
- }
12732
- for (const path of external.stripped) {
12733
- lines2.push(` ${status.success(`${path} \u2014 harness sections removed (\uBCF8\uBB38 \uBCF4\uC874)`)}`);
12734
- }
12735
- for (const path of external.unjudged) {
12736
- lines2.push(
12737
- ` ${c.yellow("\u2298")} ${path} kept \u2014 harness sections could not be separated (a file or template could not be read, or the write failed). Remove manually if intended.`
12738
- );
12739
- }
12740
- const outsideGroups = /* @__PURE__ */ new Map();
12741
- for (const o of external.outside) {
12742
- const key = `${dirname6(o.path)}\0${dirname6(o.target)}`;
12743
- outsideGroups.set(key, [...outsideGroups.get(key) ?? [], o]);
12744
- }
12745
- for (const group of outsideGroups.values()) {
12746
- const first = group[0];
12747
- if (!first) continue;
12748
- if (group.length === 1) {
12749
- lines2.push(
12750
- ` ${c.yellow("\u2298")} ${first.path} kept \u2014 link target is outside the project (${first.target}). Not touched; remove manually if intended.`
12751
- );
12752
- } else {
12753
- lines2.push(
12754
- ` ${c.yellow("\u2298")} ${dirname6(first.path)}/ kept ${group.length} file(s) \u2014 link target is outside the project (${dirname6(first.target)}). Not touched; remove manually if intended.`
12755
- );
12756
- }
12757
- }
12758
- for (const path of external.kept) {
12759
- lines2.push(
12760
- ` ${c.yellow("\u2298")} ${path} kept \u2014 modified since install. Remove manually if intended.`
12761
- );
12762
- }
12763
- return lines2;
12764
- }
12765
13430
  function hasRootImport(projectDir) {
12766
13431
  const current = readRootClaudeMd(projectDir);
12767
13432
  return current !== null && stripHarnessImport(current) !== null;
12768
13433
  }
12769
13434
  function readRootClaudeMd(projectDir) {
12770
13435
  const path = join34(projectDir, "CLAUDE.md");
12771
- if (!existsSync25(path)) return null;
13436
+ if (!existsSync26(path)) return null;
12772
13437
  try {
12773
13438
  return readFileSync26(path, "utf8");
12774
13439
  } catch {
12775
13440
  return null;
12776
13441
  }
12777
13442
  }
12778
- function rootClaudeMdModified(log, projectDir) {
12779
- const rootMd = log.templates.rootClaudeMd;
12780
- if (!rootMd) return false;
12781
- const path = join34(projectDir, rootMd.path);
12782
- if (!existsSync25(path)) return false;
12783
- return hashContent(readFileSync26(path, "utf8")) !== rootMd.sha256;
12784
- }
12785
13443
  function defaultSpawn2(cmd, args) {
12786
13444
  return spawnSync2(cmd, [...args], { encoding: "utf8", stdio: "pipe", timeout: 12e4 });
12787
13445
  }
12788
13446
  function defaultRm(path) {
12789
- if (existsSync25(path)) {
12790
- rmSync5(path, { recursive: true, force: true });
13447
+ if (existsSync26(path)) {
13448
+ rmSync4(path, { recursive: true, force: true });
12791
13449
  }
12792
13450
  }
12793
13451
  function registerUninstallCommand(cli) {
@@ -12795,10 +13453,10 @@ function registerUninstallCommand(cli) {
12795
13453
  default: process.cwd()
12796
13454
  }).option("--dry-run", "[Mode] List reverse steps without executing").option(
12797
13455
  "--keep-templates",
12798
- "[Mode] Keep `.claude/`, `.codex/`, `.opencode/` templates (remove only external assets). Without a terminal it still needs --yes, --only, --cli or --dry-run"
13456
+ "[Removed in v26.169.0] Rejected \u2014 prints the `--only <ids>` that removes only the external assets"
12799
13457
  ).option(
12800
13458
  "--only <ids>",
12801
- "[Scope] Remove only these assets (comma-separated ids from `agent-harness list`). Templates untouched"
13459
+ "[Scope] Remove only these assets (comma-separated ids from `agent-harness list`). Harness files untouched"
12802
13460
  ).option(
12803
13461
  "--cli <name>",
12804
13462
  `[Scope] Remove one CLI only (${CLI_BASES.join(" | ")}). Shared files stay until the last user leaves`
@@ -12809,6 +13467,7 @@ function registerUninstallCommand(cli) {
12809
13467
  function shouldRunInteractive(options, isTty) {
12810
13468
  if (!isTty) return false;
12811
13469
  if (options.yes || options.dryRun) return false;
13470
+ if (options.keepTemplates) return false;
12812
13471
  return options.only === void 0 && options.cli === void 0;
12813
13472
  }
12814
13473
  async function dispatchUninstall(options) {
@@ -12924,11 +13583,24 @@ function installedTargetState(projectDir) {
12924
13583
  const log = readInstallLog(projectDir);
12925
13584
  if (!log) return { installed: [], projectScoped: [] };
12926
13585
  const mcp = recordedMcpAssetIds(log);
13586
+ const skills = recordedBundledSkillIds(log);
12927
13587
  return {
12928
- installed: [...log.assets.map((a) => a.id), ...mcp],
12929
- projectScoped: [...log.assets.filter((a) => a.scope !== "global").map((a) => a.id), ...mcp]
13588
+ installed: [...log.assets.map((a) => a.id), ...mcp, ...skills],
13589
+ projectScoped: [
13590
+ ...log.assets.filter((a) => a.scope !== "global").map((a) => a.id),
13591
+ ...mcp,
13592
+ ...skills
13593
+ ]
12930
13594
  };
12931
13595
  }
13596
+ function recordedBundledSkillIds(log) {
13597
+ if (log === null) return [];
13598
+ return INTERNAL_BUNDLED_SKILL_IDS.filter(
13599
+ (id) => [`.claude/skills/${id}/SKILL.md`, `.agents/skills/${id}/SKILL.md`].some(
13600
+ (path) => recorded(log, path).state === "sha"
13601
+ )
13602
+ );
13603
+ }
12932
13604
  function wizardRecommended(tracks) {
12933
13605
  return [
12934
13606
  .../* @__PURE__ */ new Set([
@@ -12966,6 +13638,7 @@ function classifyUpdateIntent(log, confirmed, legacyTracks = []) {
12966
13638
  if (recorded2.some((a) => a.scope !== "global" && !picked.has(a.id) && !out.has(a.id)))
12967
13639
  return "add";
12968
13640
  if (recordedMcpAssetIds(log).some((id) => !picked.has(id) && !out.has(id))) return "add";
13641
+ if (recordedBundledSkillIds(log).some((id) => !picked.has(id) && !out.has(id))) return "add";
12969
13642
  const offered = new Set(listBaselineTargets({ tracks: confirmed.tracks }).map((t) => t.id));
12970
13643
  const recordedBaseline = [...out].filter((id) => offered.has(id));
12971
13644
  if (!sameSet(confirmed.baselineExclude, recordedBaseline)) return "add";
@@ -12983,7 +13656,9 @@ function coveredByRecord(log, legacyTracks) {
12983
13656
  ...(log?.assets ?? []).map((a) => a.id),
12984
13657
  // #709 — 기록된 선택 MCP 서버 · 기록 트랙의 기본 MCP 행은 refresh 가 그대로 둔다(`.mcp.json` 몫 기록 · 트랙 표)
12985
13658
  ...recordedMcpAssetIds(log),
12986
- ...trackDefaultMcpAssetIds(resolveBundleRoot(), recordTracks)
13659
+ ...trackDefaultMcpAssetIds(resolveBundleRoot(), recordTracks),
13660
+ // #760 — 기록된 번들 스킬은 refresh 가 그대로 갱신한다(`skillFiles` · `externalFiles` 기준선)
13661
+ ...recordedBundledSkillIds(log)
12987
13662
  ]);
12988
13663
  for (const id of recommendedExternalAssets(recordTracks)) {
12989
13664
  const asset = EXTERNAL_ASSETS.find((a) => a.id === id);
@@ -13253,7 +13928,9 @@ async function confirmUpdate(input) {
13253
13928
  ...baselineExclude,
13254
13929
  ...unchecked(log, tracks, assetIds, baselineIds),
13255
13930
  // #709 — 기록된 선택 MCP 서버의 체크를 풀었으면 뺀다(opt-in 이라 추천 대비 빼기에 안 든다)
13256
- ...recordedMcpAssetIds(log).filter((id) => !assetIds.includes(id))
13931
+ ...recordedMcpAssetIds(log).filter((id) => !assetIds.includes(id)),
13932
+ // #760 — 기록된 번들 스킬(opt-in 포함)의 체크를 풀었으면 뺀다 — 같은 이유
13933
+ ...recordedBundledSkillIds(log).filter((id) => !assetIds.includes(id))
13257
13934
  ])
13258
13935
  ];
13259
13936
  const options = {