@uzysjung/agent-harness 26.148.0 → 26.149.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
@@ -56,7 +56,7 @@ import {
56
56
  summarizeContextCost,
57
57
  upsertHarnessImport,
58
58
  writeInstallLog
59
- } from "./chunk-PTUM2NI3.js";
59
+ } from "./chunk-YSW3OLH4.js";
60
60
 
61
61
  // node_modules/sisteransi/src/index.js
62
62
  var require_src = __commonJS({
@@ -741,7 +741,7 @@ var cac = (name = "") => new CAC(name);
741
741
  // package.json
742
742
  var package_default = {
743
743
  name: "@uzysjung/agent-harness",
744
- version: "26.148.0",
744
+ version: "26.149.0",
745
745
  description: "Curate vetted AI-coding skills & plugins by your tech stack \u2014 install only what you need, across Claude Code, Codex, OpenCode & Antigravity",
746
746
  type: "module",
747
747
  publishConfig: {
@@ -776,6 +776,7 @@ var package_default = {
776
776
  "cost:report": "npm run build && node scripts/context-cost-report.mjs",
777
777
  "cost:baseline": "npm run build && node scripts/context-cost-baseline.mjs",
778
778
  "assets:history": "bash scripts/asset-history.sh",
779
+ "release:audit": "node scripts/release-audit.mjs",
779
780
  security: "npm audit --omit=dev --audit-level=high && npx --yes ecc-agentshield@1.4.0 scan --baseline security-baseline.json --gate",
780
781
  "security:baseline": "npx --yes ecc-agentshield@1.4.0 scan --save-baseline security-baseline.json"
781
782
  },
@@ -981,9 +982,9 @@ import {
981
982
  chmodSync as chmodSync2,
982
983
  existsSync as existsSync13,
983
984
  mkdirSync as mkdirSync4,
984
- readdirSync as readdirSync3,
985
+ readdirSync as readdirSync4,
985
986
  readFileSync as readFileSync12,
986
- unlinkSync as unlinkSync2,
987
+ unlinkSync as unlinkSync3,
987
988
  writeFileSync as writeFileSync6
988
989
  } from "fs";
989
990
  import { basename as basename5, dirname as dirname4, join as join12, resolve } from "path";
@@ -1230,7 +1231,7 @@ function writeRules(harnessRoot, projectDir, writer) {
1230
1231
  template,
1231
1232
  claudeMd,
1232
1233
  projectName: basename(projectDir),
1233
- projectContext: renderFillScaffold()
1234
+ projectContext: renderFillScaffold("antigravity-rule")
1234
1235
  });
1235
1236
  return writer.write(target, rulesOut) ? target : null;
1236
1237
  }
@@ -1376,7 +1377,7 @@ function runCodexTransform(params) {
1376
1377
  template: agentsTemplate,
1377
1378
  claudeMd,
1378
1379
  projectName,
1379
- projectContext: renderFillScaffold(),
1380
+ projectContext: renderFillScaffold("agents-md"),
1380
1381
  // Codex 는 룰 디렉터리가 없다 — 룰이 AGENTS.md 본문에 들어가야 도달한다(§Harness Rules).
1381
1382
  harnessRules: renderRulesBlock(portRules(harnessRoot, rules))
1382
1383
  });
@@ -1441,7 +1442,7 @@ function readOptionalJson(path) {
1441
1442
 
1442
1443
  // src/opencode/transform.ts
1443
1444
  init_esm_shims();
1444
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1445
+ import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync6, unlinkSync } from "fs";
1445
1446
  import { basename as basename3, join as join7 } from "path";
1446
1447
 
1447
1448
  // src/opencode/agents-md.ts
@@ -1455,72 +1456,6 @@ function renderAgentsMd2(params) {
1455
1456
  return renameSlashes2(replaced);
1456
1457
  }
1457
1458
 
1458
- // src/opencode/commands.ts
1459
- init_esm_shims();
1460
- function renderCommandFromSkill(source, id, opts) {
1461
- const { description, body } = parseSkillFrontmatter(source);
1462
- const finalDescription = description || `${id} (dev-method skill, OpenCode command fallback)`;
1463
- const escapedDesc = finalDescription.replace(/"/g, '\\"');
1464
- const renamedBody = renameSlashes2(body).trimEnd();
1465
- const agent = opts?.shellDependent ? "build" : "plan";
1466
- return [
1467
- "---",
1468
- `description: "${escapedDesc}"`,
1469
- `agent: ${agent}`,
1470
- "---",
1471
- "",
1472
- renamedBody,
1473
- ""
1474
- ].join("\n");
1475
- }
1476
- function parseSkillFrontmatter(source) {
1477
- const lines = source.split(/\r?\n/);
1478
- if (lines[0] !== "---") {
1479
- const firstLine = lines[0] ?? "";
1480
- return { description: firstLine.trim(), body: lines.slice(1).join("\n") };
1481
- }
1482
- let secondDelimAt = -1;
1483
- let description = "";
1484
- for (let i = 1; i < lines.length; i++) {
1485
- const line = lines[i] ?? "";
1486
- if (line === "---") {
1487
- secondDelimAt = i;
1488
- break;
1489
- }
1490
- const inline = line.match(/^description:\s*(.+)$/);
1491
- if (!inline) {
1492
- continue;
1493
- }
1494
- const raw = (inline[1] ?? "").trim();
1495
- if (raw === ">-" || raw === ">" || raw === "|" || raw === "|-") {
1496
- const collected = [];
1497
- for (let j3 = i + 1; j3 < lines.length; j3++) {
1498
- const next = lines[j3] ?? "";
1499
- if (next === "---") {
1500
- break;
1501
- }
1502
- if (next.trim() === "" || /^\s/.test(next)) {
1503
- collected.push(next.trim());
1504
- } else {
1505
- break;
1506
- }
1507
- }
1508
- description = collected.join(" ").replace(/\s+/g, " ").trim();
1509
- } else {
1510
- description = stripQuotes(raw);
1511
- }
1512
- }
1513
- const body = secondDelimAt >= 0 ? lines.slice(secondDelimAt + 1).join("\n").replace(/^\n+/, "") : source;
1514
- return { description, body };
1515
- }
1516
- function stripQuotes(raw) {
1517
- const trimmed = raw.trim();
1518
- if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
1519
- return trimmed.slice(1, -1);
1520
- }
1521
- return trimmed;
1522
- }
1523
-
1524
1459
  // src/opencode/opencode-json.ts
1525
1460
  init_esm_shims();
1526
1461
  function renderOpencodeJson(params) {
@@ -1564,7 +1499,7 @@ function runOpencodeTransform(params) {
1564
1499
  template: agentsTemplate,
1565
1500
  claudeMd,
1566
1501
  projectName,
1567
- projectContext: renderFillScaffold(),
1502
+ projectContext: renderFillScaffold("agents-md"),
1568
1503
  // codex 와 **같은 파일**(프로젝트 루트 `AGENTS.md`)이다. 두 transform 이 서로 다른 본문을
1569
1504
  // 쓰면 나중에 도는 쪽이 앞선 쪽을 덮어써, codex+opencode 조합에서 룰이 통째로 사라진다
1570
1505
  // (독립 검증 C-1 실측). 같은 내용을 쓰면 순서가 결과를 바꾸지 않는다.
@@ -1573,27 +1508,37 @@ function runOpencodeTransform(params) {
1573
1508
  writer.write(agentsMdPath, agentsMdOut);
1574
1509
  const opencodeJsonPath = join7(projectDir, "opencode.json");
1575
1510
  writer.write(opencodeJsonPath, renderOpencodeJson({ template: opencodeTemplate, mcp }));
1576
- const cmdDir = join7(projectDir, ".opencode/commands");
1577
- if (!refreshOnly) ensureDir(cmdDir);
1578
- const commandFiles = [];
1511
+ const skillFiles = [];
1579
1512
  for (const id of selectedInternalSkills) {
1580
1513
  const src = join7(harnessRoot, "templates/skills", id, "SKILL.md");
1581
1514
  if (!existsSync7(src)) {
1582
1515
  continue;
1583
1516
  }
1584
- const target = join7(cmdDir, `${id}.md`);
1585
- const shellDependent = existsSync7(join7(harnessRoot, "templates/skills", id, "scripts"));
1586
- const wrote = writer.write(
1587
- target,
1588
- renderCommandFromSkill(readFileSync6(src, "utf8"), id, { shellDependent })
1589
- );
1590
- if (!wrote) continue;
1591
- commandFiles.push(target);
1517
+ const target = join7(projectDir, ".agents", "skills", id, "SKILL.md");
1518
+ if (!writer.write(target, renderBundledSkill(readFileSync6(src, "utf8")))) continue;
1519
+ skillFiles.push(target);
1520
+ }
1521
+ const retiredCommands = [];
1522
+ const cmdDir = join7(projectDir, ".opencode/commands");
1523
+ if (existsSync7(cmdDir)) {
1524
+ for (const entry of readdirSync(cmdDir)) {
1525
+ if (!entry.endsWith(".md")) continue;
1526
+ const id = entry.slice(0, -3);
1527
+ if (!existsSync7(join7(harnessRoot, "templates/skills", id, "SKILL.md"))) continue;
1528
+ const victim = join7(cmdDir, entry);
1529
+ try {
1530
+ backupFile(victim);
1531
+ unlinkSync(victim);
1532
+ retiredCommands.push(victim);
1533
+ } catch {
1534
+ }
1535
+ }
1592
1536
  }
1593
1537
  return {
1594
1538
  agentsMdPath,
1595
1539
  opencodeJsonPath,
1596
- commandFiles,
1540
+ skillFiles,
1541
+ retiredCommands,
1597
1542
  ownership: writer.result()
1598
1543
  };
1599
1544
  }
@@ -1754,8 +1699,8 @@ function addGitignoreEnv(projectDir) {
1754
1699
  if (GITIGNORE_ENV_PATTERN.test(content)) {
1755
1700
  return false;
1756
1701
  }
1757
- const sep2 = content.endsWith("\n") ? "" : "\n";
1758
- appendFileSync(path, `${sep2}
1702
+ const sep3 = content.endsWith("\n") ? "" : "\n";
1703
+ appendFileSync(path, `${sep3}
1759
1704
  # Secret env (auto-added by agent-harness install)
1760
1705
  .env
1761
1706
  `);
@@ -1776,9 +1721,9 @@ function addGitignoreAgentArtifacts(projectDir) {
1776
1721
  if (missing.length === 0) {
1777
1722
  return [];
1778
1723
  }
1779
- const sep2 = content.endsWith("\n") ? "" : "\n";
1724
+ const sep3 = content.endsWith("\n") ? "" : "\n";
1780
1725
  const block = [GITIGNORE_AGENT_ARTIFACT_HEADER, ...missing].join("\n");
1781
- appendFileSync(path, `${sep2}
1726
+ appendFileSync(path, `${sep3}
1782
1727
  ${block}
1783
1728
  `);
1784
1729
  return [...missing];
@@ -1787,7 +1732,7 @@ ${block}
1787
1732
  // src/external-installer.ts
1788
1733
  init_esm_shims();
1789
1734
  import { spawnSync } from "child_process";
1790
- import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync8 } from "fs";
1735
+ import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync8 } from "fs";
1791
1736
  import { homedir as homedir2 } from "os";
1792
1737
  import { join as join9 } from "path";
1793
1738
  var DEFAULT_SPAWN_TIMEOUT_MS = 12e4;
@@ -1890,6 +1835,45 @@ var SKILLS_CLI_VERSION = "1.5.11";
1890
1835
  function skillsCliSpec() {
1891
1836
  return `skills@${SKILLS_CLI_VERSION}`;
1892
1837
  }
1838
+ function refreshExternalSkills(projectDir, deps = {}) {
1839
+ const none = { attempted: 0, refreshed: 0, failed: [], notInCatalog: [] };
1840
+ const log = (deps.readLog ?? readInstallLog)(projectDir);
1841
+ if (!log) {
1842
+ return { ...none, unknown: true };
1843
+ }
1844
+ const installedIds = new Set(log.assets.filter((a) => a.method === "skill").map((a) => a.id));
1845
+ const catalog = deps.assets ?? EXTERNAL_ASSETS;
1846
+ const targets = catalog.filter((a) => a.method.kind === "skill" && installedIds.has(a.id));
1847
+ const inCatalog = new Set(targets.map((a) => a.id));
1848
+ const notInCatalog = [...installedIds].filter((id) => !inCatalog.has(id));
1849
+ if (targets.length === 0) {
1850
+ return { ...none, notInCatalog, unknown: false };
1851
+ }
1852
+ const report = (deps.run ?? runExternalInstall)(
1853
+ {
1854
+ tracks: log.spec.tracks,
1855
+ // `forceInclude` 가 조건 판정을 앞지르므로 옵션 값은 결과에 영향을 주지 않는다.
1856
+ options: DEFAULT_OPTIONS,
1857
+ cli: log.spec.cli,
1858
+ userOverride: { forceInclude: [...installedIds], forceExclude: [] },
1859
+ scope: log.scope,
1860
+ projectDir
1861
+ },
1862
+ {
1863
+ assets: targets,
1864
+ ...deps.spawn ? { spawn: deps.spawn } : {},
1865
+ ...deps.log ? { log: deps.log } : {},
1866
+ ...deps.warn ? { warn: deps.warn } : {}
1867
+ }
1868
+ );
1869
+ return {
1870
+ attempted: report.attempted.length,
1871
+ refreshed: report.succeeded,
1872
+ failed: report.attempted.filter((r) => !r.ok).map((r) => ({ id: r.asset.id, message: r.message ?? "failed" })),
1873
+ notInCatalog,
1874
+ unknown: false
1875
+ };
1876
+ }
1893
1877
  function buildSkillArgs(method, cli2, scope) {
1894
1878
  const args = [skillsCliSpec(), "add", method.source];
1895
1879
  if (method.skill) {
@@ -1899,6 +1883,7 @@ function buildSkillArgs(method, cli2, scope) {
1899
1883
  for (const c3 of cli2) {
1900
1884
  args.push("--agent", SKILLS_CLI_AGENT_MAP[c3] ?? c3);
1901
1885
  }
1886
+ args.push("--copy");
1902
1887
  }
1903
1888
  if (scope === "global") {
1904
1889
  args.push("-g");
@@ -1958,7 +1943,7 @@ function detectVersion(method, spawn) {
1958
1943
  const marketplaceShort = method.pluginId.slice(at2 + 1);
1959
1944
  const cacheBase = join9(homedir2(), ".claude/plugins/cache", marketplaceShort, plugin);
1960
1945
  if (!existsSync9(cacheBase)) return void 0;
1961
- const versions = readdirSync(cacheBase).filter((v2) => /^\d/.test(v2)).sort();
1946
+ const versions = readdirSync2(cacheBase).filter((v2) => /^\d/.test(v2)).sort();
1962
1947
  return versions.at(-1);
1963
1948
  }
1964
1949
  case "npm": {
@@ -2107,9 +2092,9 @@ import {
2107
2092
  copyFileSync,
2108
2093
  existsSync as existsSync12,
2109
2094
  mkdirSync as mkdirSync3,
2110
- readdirSync as readdirSync2,
2095
+ readdirSync as readdirSync3,
2111
2096
  readFileSync as readFileSync11,
2112
- unlinkSync,
2097
+ unlinkSync as unlinkSync2,
2113
2098
  writeFileSync as writeFileSync5
2114
2099
  } from "fs";
2115
2100
  import { basename as basename4, dirname as dirname3, join as join11 } from "path";
@@ -2119,10 +2104,11 @@ function buildUpdateSpec(projectDir, tracks) {
2119
2104
  tracks: [...tracks],
2120
2105
  options: DEFAULT_OPTIONS,
2121
2106
  cli: ["claude"],
2122
- projectDir
2107
+ projectDir,
2108
+ scope: readInstallLog(projectDir)?.scope ?? "project"
2123
2109
  };
2124
2110
  }
2125
- function runUpdateMode(projectDir, templatesDir, harnessRoot) {
2111
+ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}) {
2126
2112
  const claudeDir = join11(projectDir, ".claude");
2127
2113
  const report = {
2128
2114
  updated: {},
@@ -2137,6 +2123,10 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot) {
2137
2123
  policyBackedUp: [],
2138
2124
  externalUpdated: 0,
2139
2125
  externalBackedUp: [],
2126
+ externalSkillsRefreshed: 0,
2127
+ externalSkillsFailed: [],
2128
+ externalSkillsNotInCatalog: [],
2129
+ externalSkillsUnknown: false,
2140
2130
  foreignOwned: [],
2141
2131
  installedNew: [],
2142
2132
  restored: [],
@@ -2192,6 +2182,11 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot) {
2192
2182
  if (!saidBySlotRow.has(f) && !merged.includes(f)) merged.push(f);
2193
2183
  }
2194
2184
  report.foreignOwned = merged;
2185
+ const skillRefresh = (deps.refreshSkills ?? refreshExternalSkills)(projectDir);
2186
+ report.externalSkillsRefreshed = skillRefresh.refreshed;
2187
+ report.externalSkillsFailed = skillRefresh.failed;
2188
+ report.externalSkillsNotInCatalog = skillRefresh.notInCatalog;
2189
+ report.externalSkillsUnknown = skillRefresh.unknown;
2195
2190
  return report;
2196
2191
  }
2197
2192
  function installNewAssets(projectDir, templatesDir) {
@@ -2324,7 +2319,7 @@ function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2324
2319
  if (!existsSync12(target) || !existsSync12(source)) return { updated: 0, backedUp: [] };
2325
2320
  let updated = 0;
2326
2321
  const backedUp = [];
2327
- for (const file of readdirSync2(target)) {
2322
+ for (const file of readdirSync3(target)) {
2328
2323
  if (!file.endsWith(ext)) continue;
2329
2324
  const targetFile = join11(target, file);
2330
2325
  const sourceFile = join11(source, file);
@@ -2348,7 +2343,7 @@ function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Da
2348
2343
  const backedUp = [];
2349
2344
  const skippedLinks = [];
2350
2345
  const foreignOwned = [];
2351
- for (const skill of readdirSync2(sourceDir, { withFileTypes: true })) {
2346
+ for (const skill of readdirSync3(sourceDir, { withFileTypes: true })) {
2352
2347
  if (!skill.isDirectory()) continue;
2353
2348
  const targetSkill = join11(targetDir, skill.name);
2354
2349
  if (occupiedByNonDirectory(targetSkill)) {
@@ -2418,7 +2413,7 @@ function refreshSkillBaseline(projectDir) {
2418
2413
  function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
2419
2414
  if (!existsSync12(target) || !existsSync12(source)) return [];
2420
2415
  const removed = [];
2421
- for (const file of readdirSync2(target)) {
2416
+ for (const file of readdirSync3(target)) {
2422
2417
  if (!file.endsWith(ext)) continue;
2423
2418
  const sourceFile = join11(source, file);
2424
2419
  if (!existsSync12(sourceFile)) {
@@ -2427,7 +2422,7 @@ function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()
2427
2422
  try {
2428
2423
  const current = readFileSync11(targetFile, "utf8");
2429
2424
  if (!isHarnessOwned2(ctx, file, current)) backupFile(targetFile, now);
2430
- unlinkSync(targetFile);
2425
+ unlinkSync2(targetFile);
2431
2426
  removed.push(file);
2432
2427
  } catch {
2433
2428
  }
@@ -2440,7 +2435,7 @@ function retireMcpAllowlist(projectDir, now = /* @__PURE__ */ new Date()) {
2440
2435
  if (!existsSync12(target)) return null;
2441
2436
  try {
2442
2437
  const backup = backupFile(target, now);
2443
- unlinkSync(target);
2438
+ unlinkSync2(target);
2444
2439
  return backup;
2445
2440
  } catch {
2446
2441
  return null;
@@ -2604,7 +2599,7 @@ function applySupersededCleanup(spec, projectDir, found) {
2604
2599
  const kept = [];
2605
2600
  for (const asset of found) {
2606
2601
  try {
2607
- unlinkSync2(join12(projectDir, asset.target));
2602
+ unlinkSync3(join12(projectDir, asset.target));
2608
2603
  removed.push(asset.target);
2609
2604
  } catch {
2610
2605
  kept.push(asset.target);
@@ -2942,7 +2937,7 @@ function chmodHooksSync(hookDir) {
2942
2937
  }
2943
2938
  }
2944
2939
  function listHookFiles(hookDir) {
2945
- return readdirSync3(hookDir, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".sh")).map((e2) => resolve(hookDir, e2.name));
2940
+ return readdirSync4(hookDir, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".sh")).map((e2) => resolve(hookDir, e2.name));
2946
2941
  }
2947
2942
 
2948
2943
  // src/commands/install-render.ts
@@ -3116,7 +3111,24 @@ function renderCliArtifacts(log, spec, report) {
3116
3111
  }
3117
3112
  if (report.opencode) {
3118
3113
  log(assetRow("success", "opencode.json", "$schema + 5 keys"));
3119
- log(assetRow("success", ".opencode/commands/", `${report.opencode.commandFiles.length} files`));
3114
+ if (report.opencode.skillFiles.length > 0) {
3115
+ log(
3116
+ assetRow(
3117
+ "success",
3118
+ ".agents/skills/",
3119
+ `${report.opencode.skillFiles.length} dev-method skills (codex\xB7antigravity \uC640 \uAC19\uC740 \uC790\uB9AC)`
3120
+ )
3121
+ );
3122
+ }
3123
+ if (report.opencode.retiredCommands.length > 0) {
3124
+ log(
3125
+ assetRow(
3126
+ "skip",
3127
+ ".opencode/commands/",
3128
+ `${report.opencode.retiredCommands.length} retired \xB7 \uAC19\uC740 \uC2A4\uD0AC\uC774 .agents/skills/ \uB85C \uC62E\uACA8\uC84C\uB2E4 (\uBC31\uC5C5 \uB0A8\uAE40)`
3129
+ )
3130
+ );
3131
+ }
3120
3132
  }
3121
3133
  if (report.antigravity) {
3122
3134
  if (report.antigravity.rulesFile) {
@@ -3350,6 +3362,39 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false, claud
3350
3362
  )
3351
3363
  );
3352
3364
  }
3365
+ if (baseline.updateMode.externalSkillsRefreshed > 0) {
3366
+ log(
3367
+ assetRow(
3368
+ "success",
3369
+ "external skills",
3370
+ `${baseline.updateMode.externalSkillsRefreshed} refreshed from upstream`
3371
+ )
3372
+ );
3373
+ }
3374
+ if (baseline.updateMode.externalSkillsFailed.length > 0) {
3375
+ const failed = baseline.updateMode.externalSkillsFailed;
3376
+ const head = failed[0];
3377
+ const meta = failed.length === 1 ? `${head.id}: ${head.message}` : `${failed.map((f) => f.id).join(", ")} (${failed.length}\uAC74) \xB7 \uC608: ${head.message}`;
3378
+ log(assetRow("skip", "external skills", `${meta} \xB7 \uB2E4\uC74C update \uC5D0\uC11C \uB2E4\uC2DC \uC2DC\uB3C4\uD55C\uB2E4`));
3379
+ }
3380
+ if (baseline.updateMode.externalSkillsNotInCatalog.length > 0) {
3381
+ log(
3382
+ assetRow(
3383
+ "skip",
3384
+ "external skills",
3385
+ `${baseline.updateMode.externalSkillsNotInCatalog.join(", ")} \xB7 \uCE74\uD0C8\uB85C\uADF8\uC5D0 \uC5C6\uC5B4 \uAC31\uC2E0 \uB300\uC0C1\uC774 \uC544\uB2C8\uB2E4`
3386
+ )
3387
+ );
3388
+ }
3389
+ if (baseline.updateMode.externalSkillsUnknown) {
3390
+ log(
3391
+ assetRow(
3392
+ "skip",
3393
+ "external skills",
3394
+ "\uC124\uCE58 \uAE30\uB85D\uC774 \uC5C6\uC5B4 \uAC31\uC2E0 \uB300\uC0C1\uC744 \uD310\uC815\uD560 \uC218 \uC5C6\uB2E4 \xB7 `agent-harness install` \uB85C \uB2E4\uC2DC \uAE54\uBA74 \uAE30\uB85D\uB41C\uB2E4"
3395
+ )
3396
+ );
3397
+ }
3353
3398
  return;
3354
3399
  }
3355
3400
  if (baseline.backups) {
@@ -3816,8 +3861,16 @@ function registerListCommand(cli2) {
3816
3861
  // src/commands/uninstall.ts
3817
3862
  init_esm_shims();
3818
3863
  import { spawnSync as spawnSync2 } from "child_process";
3819
- import { existsSync as existsSync15, readFileSync as readFileSync14, rmSync, writeFileSync as writeFileSync7 } from "fs";
3820
- import { join as join14, resolve as resolve4 } from "path";
3864
+ import {
3865
+ existsSync as existsSync15,
3866
+ lstatSync as lstatSync2,
3867
+ readdirSync as readdirSync5,
3868
+ readFileSync as readFileSync14,
3869
+ rmdirSync,
3870
+ rmSync,
3871
+ writeFileSync as writeFileSync7
3872
+ } from "fs";
3873
+ import { dirname as dirname5, join as join14, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
3821
3874
 
3822
3875
  // src/uninstall-interactive.ts
3823
3876
  init_esm_shims();
@@ -4953,8 +5006,13 @@ function uninstallAction(options, deps = {}) {
4953
5006
  const logSurvives = selectedIds !== null;
4954
5007
  const { succeeded, failed, removedIds } = executeReverse(plan, log, logSurvives);
4955
5008
  if (!keepTemplates) {
4956
- const { rootClaudeMdKept, importStripped } = removeTemplates(installLog, projectDir, rm);
5009
+ const { rootClaudeMdKept, importStripped, external } = removeTemplates(
5010
+ installLog,
5011
+ projectDir,
5012
+ rm
5013
+ );
4957
5014
  log(` ${status.success(`templates removed: ${formatTemplateList(installLog)}`)}`);
5015
+ for (const line of externalRemovalLines(external)) log(line);
4958
5016
  if (importStripped) {
4959
5017
  log(` ${status.success("CLAUDE.md \u2014 harness @import removed (\uBCF8\uBB38 \uBCF4\uC874)")}`);
4960
5018
  }
@@ -5080,6 +5138,7 @@ function dryRunLines(plan, installLog, projectDir, keepTemplates, rootFiles) {
5080
5138
  if (hasRootImport(projectDir)) {
5081
5139
  lines.push(" \u25CB strip harness @import from CLAUDE.md (\uBCF8\uBB38 \uBCF4\uC874)");
5082
5140
  }
5141
+ lines.push(...previewExternalLines(installLog, projectDir));
5083
5142
  }
5084
5143
  lines.push(...advisoryLines(plan, projectDir, rootFiles), "");
5085
5144
  return lines;
@@ -5205,13 +5264,46 @@ function removeTemplates(log, projectDir, rm) {
5205
5264
  rm(join14(projectDir, log.templates.claudeDir));
5206
5265
  if (log.templates.codexDir) rm(join14(projectDir, log.templates.codexDir));
5207
5266
  if (log.templates.opencodeDir) rm(join14(projectDir, log.templates.opencodeDir));
5267
+ const external = removeExternalFiles(log, projectDir, rm);
5208
5268
  const importStripped = stripRootImport(projectDir);
5209
5269
  const rootMd = log.templates.rootClaudeMd;
5210
5270
  if (rootMd) {
5211
- if (rootClaudeMdModified(log, projectDir)) return { rootClaudeMdKept: true, importStripped };
5271
+ if (rootClaudeMdModified(log, projectDir))
5272
+ return { rootClaudeMdKept: true, importStripped, external };
5212
5273
  rm(join14(projectDir, rootMd.path));
5213
5274
  }
5214
- return { rootClaudeMdKept: false, importStripped };
5275
+ return { rootClaudeMdKept: false, importStripped, external };
5276
+ }
5277
+ function removeExternalFiles(log, projectDir, rm) {
5278
+ const removed = [];
5279
+ const kept = [];
5280
+ for (const { path, sha256 } of log.externalFiles ?? []) {
5281
+ const abs = join14(projectDir, path);
5282
+ if (!existsSync15(abs)) continue;
5283
+ if (!lstatSync2(abs).isFile()) continue;
5284
+ if (hashContent(readFileSync14(abs, "utf8")) !== sha256) {
5285
+ kept.push(path);
5286
+ continue;
5287
+ }
5288
+ rm(abs);
5289
+ removed.push(path);
5290
+ }
5291
+ for (const path of removed) pruneEmptyDirsUpward(projectDir, dirname5(join14(projectDir, path)));
5292
+ return { removed, kept };
5293
+ }
5294
+ function pruneEmptyDirsUpward(projectDir, startDir) {
5295
+ let cursor = startDir;
5296
+ while (true) {
5297
+ const rel = relative2(projectDir, cursor);
5298
+ if (rel === "" || rel.startsWith(`..${sep2}`) || rel === "..") return;
5299
+ try {
5300
+ if (readdirSync5(cursor).length > 0) return;
5301
+ rmdirSync(cursor);
5302
+ } catch {
5303
+ return;
5304
+ }
5305
+ cursor = dirname5(cursor);
5306
+ }
5215
5307
  }
5216
5308
  function stripRootImport(projectDir) {
5217
5309
  const current = readRootClaudeMd(projectDir);
@@ -5224,6 +5316,33 @@ function stripRootImport(projectDir) {
5224
5316
  return false;
5225
5317
  }
5226
5318
  }
5319
+ function previewExternalLines(installLog, projectDir) {
5320
+ const lines = [];
5321
+ let removable = 0;
5322
+ for (const { path, sha256 } of installLog.externalFiles ?? []) {
5323
+ const abs = join14(projectDir, path);
5324
+ if (!existsSync15(abs)) continue;
5325
+ if (hashContent(readFileSync14(abs, "utf8")) !== sha256) {
5326
+ lines.push(` \u25CB keep ${path} (modified since install \u2014 preserved)`);
5327
+ continue;
5328
+ }
5329
+ removable += 1;
5330
+ }
5331
+ if (removable > 0) lines.unshift(` \u25CB remove ${removable} CLI output file(s)`);
5332
+ return lines;
5333
+ }
5334
+ function externalRemovalLines(external) {
5335
+ const lines = [];
5336
+ if (external.removed.length > 0) {
5337
+ lines.push(` ${status.success(`CLI outputs removed: ${external.removed.length} file(s)`)}`);
5338
+ }
5339
+ for (const path of external.kept) {
5340
+ lines.push(
5341
+ ` ${c.yellow("\u2298")} ${path} kept \u2014 modified since install. Remove manually if intended.`
5342
+ );
5343
+ }
5344
+ return lines;
5345
+ }
5227
5346
  function hasRootImport(projectDir) {
5228
5347
  const current = readRootClaudeMd(projectDir);
5229
5348
  return current !== null && stripHarnessImport(current) !== null;