@uzysjung/agent-harness 26.83.0 → 26.84.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/README.ko.md CHANGED
@@ -226,7 +226,7 @@ Flag:
226
226
 
227
227
  ```
228
228
  ┌──────────────────────────────────────────────────────────┐
229
- │ npx agent-harness
229
+ │ npx -y @uzysjung/agent-harness
230
230
  │ │ │
231
231
  │ ▼ │
232
232
  │ ┌─ 6-step wizard ──────────────────────────────────┐ │
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # uzys-agent-harness
2
2
 
3
- **Track-based agent harness for Claude Code, Codex, OpenCode, and Antigravity.**
3
+ **Install only the AI-coding workflows your tech stack actually needs — vetted, curated, and set up with one command across Claude Code, Codex, OpenCode & Antigravity.**
4
4
 
5
- Pick a stack track. Get a curated set of **vetted** skills, plugins, and rules you review and choose what installs wired into your project. Project scope by default; no global pollution unless you ask.
5
+ Coding agents keep getting stronger out of the box — piling on skills and MCPs you'll never use just bloats their context. And the awesome-lists have too many options to wade through. `agent-harness` curates by **tech stack**: of the vetted options, you install only what this project actually calls for. **Claude Code is first-class; Codex / OpenCode / Antigravity get the skills + rules layer.** Project scope by default no global pollution unless you ask.
6
6
 
7
7
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
8
  [![Version](https://img.shields.io/github/v/tag/uzysjung/uzys-agent-harness?label=version)](https://github.com/uzysjung/uzys-agent-harness/releases)
@@ -10,6 +10,8 @@ Pick a stack track. Get a curated set of **vetted** skills, plugins, and rules
10
10
 
11
11
  ![agent-harness demo — one-command install of vetted AI-coding workflows](https://raw.githubusercontent.com/uzysjung/uzys-agent-harness/main/docs/assets/agent-harness-demo.gif)
12
12
 
13
+ > **What "vetted" means** — ≥ 1000 GitHub stars + active maintenance + a Docker install-verification run, re-checked by CI ([catalog-verify](docs/COMPATIBILITY.md), [trust-tier-drift](.github/workflows/)). It is **not** a line-by-line security audit or a prompt-injection scan of asset contents. Treat installed assets like any third-party dependency — see [SECURITY.md](SECURITY.md).
14
+
13
15
  🇰🇷 [한국어](./README.ko.md)
14
16
 
15
17
  ---
@@ -59,6 +61,21 @@ npx -y @uzysjung/agent-harness install \
59
61
 
60
62
  ---
61
63
 
64
+ ## Installing into an existing project
65
+
66
+ `agent-harness` never silently overwrites your config. Before replacing an **editable** file whose contents differ, it writes a timestamped backup next to it — and every backup path is printed in the install summary (`backup` rows). Nothing is deleted.
67
+
68
+ | You already have… | What happens |
69
+ |---|---|
70
+ | `.claude/settings.json` with your own hooks / statusLine | Backed up to `settings.json.backup-<ts>` before update |
71
+ | Root `CLAUDE.md` (yours differs from the generated one) | Backed up to `CLAUDE.md.backup-<ts>` before the merge write |
72
+ | `.claude/` on `--reinstall` / `update` mode | The whole directory is renamed to `.claude.backup-<ts>` first |
73
+ | `.mcp.json` | Your existing MCP servers are preserved and merged, not replaced |
74
+
75
+ > Fresh project? None of this triggers — backups only protect pre-existing files.
76
+
77
+ ---
78
+
62
79
  ## Tracks
63
80
 
64
81
  Pick one or more at step 1. Each track determines which skills/plugins/rules are pre-checked in step 3.
@@ -253,7 +270,7 @@ Flags:
253
270
 
254
271
  ```
255
272
  ┌──────────────────────────────────────────────────────────┐
256
- │ npx agent-harness
273
+ │ npx -y @uzysjung/agent-harness
257
274
  │ │ │
258
275
  │ ▼ │
259
276
  │ ┌─ 6-step wizard ──────────────────────────────────┐ │
@@ -277,6 +294,20 @@ Flags:
277
294
  └──────────────────────────────────────────────────────────┘
278
295
  ```
279
296
 
297
+ After install, a `tooling` + Claude project looks like:
298
+
299
+ ```
300
+ your-project/
301
+ ├── .claude/
302
+ │ ├── rules/ # coding conventions for your stack
303
+ │ ├── agents/ # subagent definitions
304
+ │ ├── commands/uzys/ # /uzys:* commands (only if you opted into uzys-harness)
305
+ │ ├── hooks/ # gate / pre-commit hooks
306
+ │ └── settings.json # your existing one is backed up first
307
+ ├── CLAUDE.md # merged instructions (yours backed up if it differed)
308
+ └── .mcp.json # MCP servers, merged with yours
309
+ ```
310
+
280
311
  ---
281
312
 
282
313
  ## CLI support
package/dist/index.js CHANGED
@@ -698,7 +698,7 @@ var cac = (name = "") => new CAC(name);
698
698
  // package.json
699
699
  var package_default = {
700
700
  name: "@uzysjung/agent-harness",
701
- version: "26.83.0",
701
+ version: "26.84.0",
702
702
  description: "One-command installer & curator of vetted, Docker-verified AI-coding workflows across Claude Code, Codex, OpenCode & Antigravity",
703
703
  type: "module",
704
704
  publishConfig: {
@@ -933,7 +933,7 @@ import {
933
933
  existsSync as existsSync14,
934
934
  mkdirSync as mkdirSync6,
935
935
  readdirSync as readdirSync4,
936
- readFileSync as readFileSync13,
936
+ readFileSync as readFileSync14,
937
937
  writeFileSync as writeFileSync11
938
938
  } from "fs";
939
939
  import { dirname as dirname4, join as join12, resolve } from "path";
@@ -996,7 +996,7 @@ function runAntigravityOptIn(ctx) {
996
996
 
997
997
  // src/antigravity/transform.ts
998
998
  init_esm_shims();
999
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
999
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1000
1000
  import { basename, join as join3 } from "path";
1001
1001
 
1002
1002
  // src/codex/skills.ts
@@ -1049,7 +1049,7 @@ function stripQuotes(raw) {
1049
1049
 
1050
1050
  // src/fs-ops.ts
1051
1051
  init_esm_shims();
1052
- import { copyFileSync, cpSync as cpSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, renameSync } from "fs";
1052
+ import { copyFileSync, cpSync as cpSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync } from "fs";
1053
1053
  import { dirname, join as join2 } from "path";
1054
1054
  function ensureDir(path) {
1055
1055
  mkdirSync2(path, { recursive: true });
@@ -1084,6 +1084,17 @@ function copyBackupDir(target, now = /* @__PURE__ */ new Date()) {
1084
1084
  cpSync2(target, backup, { recursive: true });
1085
1085
  return backup;
1086
1086
  }
1087
+ function backupFileIfChanged(target, newContent, now = /* @__PURE__ */ new Date()) {
1088
+ if (!existsSync2(target)) {
1089
+ return null;
1090
+ }
1091
+ if (readFileSync2(target, "utf-8") === newContent) {
1092
+ return null;
1093
+ }
1094
+ const backup = `${target}.backup-${formatStamp(now)}`;
1095
+ copyFileSync(target, backup);
1096
+ return backup;
1097
+ }
1087
1098
  function formatStamp(now) {
1088
1099
  return now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z").slice(0, 15);
1089
1100
  }
@@ -1116,11 +1127,11 @@ function runAntigravityTransform(params) {
1116
1127
  const cmdSrc = join3(harnessRoot, "templates/commands/uzys", `${phase}.md`);
1117
1128
  let source = "";
1118
1129
  if (existsSync3(cmdSrc)) {
1119
- source = readFileSync2(cmdSrc, "utf8");
1130
+ source = readFileSync3(cmdSrc, "utf8");
1120
1131
  } else {
1121
1132
  const fallback = join3(harnessRoot, "templates/codex/skills", `uzys-${phase}/SKILL.md`);
1122
1133
  if (existsSync3(fallback)) {
1123
- source = readFileSync2(fallback, "utf8");
1134
+ source = readFileSync3(fallback, "utf8");
1124
1135
  }
1125
1136
  }
1126
1137
  const skillTarget = join3(skillDir, "SKILL.md");
@@ -1143,8 +1154,8 @@ function writeRules(harnessRoot, projectDir) {
1143
1154
  if (!existsSync3(claudeMdPath) || !existsSync3(templatePath)) {
1144
1155
  return null;
1145
1156
  }
1146
- const claudeMd = readFileSync2(claudeMdPath, "utf8");
1147
- const template = readFileSync2(templatePath, "utf8");
1157
+ const claudeMd = readFileSync3(claudeMdPath, "utf8");
1158
+ const template = readFileSync3(templatePath, "utf8");
1148
1159
  const rulesDir = join3(projectDir, ".agents", "rules");
1149
1160
  ensureDir(rulesDir);
1150
1161
  const target = join3(rulesDir, "uzys-harness.md");
@@ -1154,7 +1165,7 @@ function writeRules(harnessRoot, projectDir) {
1154
1165
 
1155
1166
  // src/codex/opt-in.ts
1156
1167
  init_esm_shims();
1157
- import { cpSync as cpSync3, existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
1168
+ import { cpSync as cpSync3, existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
1158
1169
  import { homedir as homedir2 } from "os";
1159
1170
  import { join as join4 } from "path";
1160
1171
 
@@ -1201,14 +1212,14 @@ var CODEX_PROMPT_PHASES = ["spec", "plan", "build", "test", "review", "ship"];
1201
1212
 
1202
1213
  // src/codex/trust-entry.ts
1203
1214
  init_esm_shims();
1204
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
1215
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1205
1216
  import { dirname as dirname2 } from "path";
1206
1217
  var TRUST_BLOCK_REGEX = /\[projects\."([^"]+)"\]/g;
1207
1218
  function registerTrustEntry(opts) {
1208
1219
  const { configPath, projectDir } = opts;
1209
1220
  try {
1210
1221
  mkdirSync3(dirname2(configPath), { recursive: true });
1211
- const existing = existsSync4(configPath) ? readFileSync3(configPath, "utf8") : "";
1222
+ const existing = existsSync4(configPath) ? readFileSync4(configPath, "utf8") : "";
1212
1223
  if (hasTrustEntry(existing, projectDir)) {
1213
1224
  return { status: "already-present" };
1214
1225
  }
@@ -1287,7 +1298,7 @@ function copyCodexPrompts(harnessRoot, projectDir, promptsTarget) {
1287
1298
  for (const phase of CODEX_PROMPT_PHASES) {
1288
1299
  const src = join4(sourceDir, `${phase}.md`);
1289
1300
  if (!existsSync5(src)) continue;
1290
- const source = readFileSync4(src, "utf8");
1301
+ const source = readFileSync5(src, "utf8");
1291
1302
  const dst = join4(promptsTarget, `uzys-${phase}.md`);
1292
1303
  writeFileSync4(dst, renderCodexPrompt({ source, phase }));
1293
1304
  count++;
@@ -1323,7 +1334,7 @@ function copyCodexSkills(projectDir, skillsTarget) {
1323
1334
 
1324
1335
  // src/codex/transform.ts
1325
1336
  init_esm_shims();
1326
- import { chmodSync, existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
1337
+ import { chmodSync, existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1327
1338
  import { basename as basename2, join as join5 } from "path";
1328
1339
 
1329
1340
  // src/codex/config-toml.ts
@@ -1421,7 +1432,7 @@ function runCodexTransform(params) {
1421
1432
  if (!existsSync6(src)) {
1422
1433
  continue;
1423
1434
  }
1424
- const ported = readFileSync5(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
1435
+ const ported = readFileSync6(src, "utf8").replace(ENV_VAR_RENAME, "CODEX_PROJECT_DIR");
1425
1436
  const target = join5(hookDir, `${hook}.sh`);
1426
1437
  writeFileSync5(target, ported);
1427
1438
  chmodSync(target, 493);
@@ -1435,11 +1446,11 @@ function runCodexTransform(params) {
1435
1446
  const cmdSrc = join5(harnessRoot, "templates/commands/uzys", `${phase}.md`);
1436
1447
  let source = "";
1437
1448
  if (existsSync6(cmdSrc)) {
1438
- source = readFileSync5(cmdSrc, "utf8");
1449
+ source = readFileSync6(cmdSrc, "utf8");
1439
1450
  } else {
1440
1451
  const fallback = join5(harnessRoot, "templates/codex/skills", `uzys-${phase}/SKILL.md`);
1441
1452
  if (existsSync6(fallback)) {
1442
- source = readFileSync5(fallback, "utf8");
1453
+ source = readFileSync6(fallback, "utf8");
1443
1454
  }
1444
1455
  }
1445
1456
  const target = join5(skillDir, "SKILL.md");
@@ -1456,7 +1467,7 @@ function runCodexTransform(params) {
1456
1467
  if (!existsSync6(cmdSrc)) {
1457
1468
  continue;
1458
1469
  }
1459
- const source = readFileSync5(cmdSrc, "utf8");
1470
+ const source = readFileSync6(cmdSrc, "utf8");
1460
1471
  const target = join5(promptDir, `uzys-${phase}.md`);
1461
1472
  writeFileSync5(target, renderCodexPrompt({ source, phase }));
1462
1473
  promptFiles.push(target);
@@ -1468,14 +1479,14 @@ function readRequired(path) {
1468
1479
  if (!existsSync6(path)) {
1469
1480
  throw new Error(`Codex transform: required source missing: ${path}`);
1470
1481
  }
1471
- return readFileSync5(path, "utf8");
1482
+ return readFileSync6(path, "utf8");
1472
1483
  }
1473
1484
  function readOptionalJson(path) {
1474
1485
  if (!existsSync6(path)) {
1475
1486
  return null;
1476
1487
  }
1477
1488
  try {
1478
- return JSON.parse(readFileSync5(path, "utf8"));
1489
+ return JSON.parse(readFileSync6(path, "utf8"));
1479
1490
  } catch {
1480
1491
  return null;
1481
1492
  }
@@ -1483,7 +1494,7 @@ function readOptionalJson(path) {
1483
1494
 
1484
1495
  // src/env-files.ts
1485
1496
  init_esm_shims();
1486
- import { appendFileSync, existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
1497
+ import { appendFileSync, existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
1487
1498
  import { join as join6 } from "path";
1488
1499
  var ENV_EXAMPLE_BODY = `# .env.example \u2014 csr-supabase Track
1489
1500
  # Copy to .env (gitignored) and fill in values: cp .env.example .env
@@ -1533,7 +1544,7 @@ function addGitignoreEnv(projectDir) {
1533
1544
  if (!existsSync7(path)) {
1534
1545
  return false;
1535
1546
  }
1536
- const content = readFileSync6(path, "utf8");
1547
+ const content = readFileSync7(path, "utf8");
1537
1548
  if (GITIGNORE_ENV_PATTERN.test(content)) {
1538
1549
  return false;
1539
1550
  }
@@ -1551,7 +1562,7 @@ function addGitignoreNpxSkillsAgents(projectDir) {
1551
1562
  if (!existsSync7(path)) {
1552
1563
  return [];
1553
1564
  }
1554
- const content = readFileSync6(path, "utf8");
1565
+ const content = readFileSync7(path, "utf8");
1555
1566
  const missing = NPX_SKILLS_AGENT_DIRS.filter((pattern) => {
1556
1567
  const lineRegex = new RegExp(`^${pattern.replace(/\./g, "\\.").replace(/\//g, "/")}\\s*$`, "m");
1557
1568
  return !lineRegex.test(content);
@@ -1577,7 +1588,7 @@ function writeMcpAllowlist(projectDir) {
1577
1588
  }
1578
1589
  let names = [];
1579
1590
  try {
1580
- const parsed = JSON.parse(readFileSync6(mcpPath, "utf8"));
1591
+ const parsed = JSON.parse(readFileSync7(mcpPath, "utf8"));
1581
1592
  names = Object.keys(parsed.mcpServers ?? {}).sort();
1582
1593
  } catch {
1583
1594
  return null;
@@ -1600,7 +1611,7 @@ function writeMcpAllowlist(projectDir) {
1600
1611
  // src/external-installer.ts
1601
1612
  init_esm_shims();
1602
1613
  import { spawnSync } from "child_process";
1603
- import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync7 } from "fs";
1614
+ import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync8 } from "fs";
1604
1615
  import { homedir as homedir3 } from "os";
1605
1616
  import { join as join7 } from "path";
1606
1617
  var DEFAULT_SPAWN_TIMEOUT_MS = 12e4;
@@ -1691,8 +1702,12 @@ var SKILLS_CLI_AGENT_MAP = {
1691
1702
  // v26.66.0 — Antigravity (Google) skills agent. `.agents/skills/` 표준 공유 (codex transform 산출과 동일).
1692
1703
  antigravity: "antigravity"
1693
1704
  };
1705
+ var SKILLS_CLI_VERSION = "1.5.11";
1706
+ function skillsCliSpec() {
1707
+ return `skills@${SKILLS_CLI_VERSION}`;
1708
+ }
1694
1709
  function buildSkillArgs(method, cli2, scope) {
1695
- const args = ["skills", "add", method.source];
1710
+ const args = [skillsCliSpec(), "add", method.source];
1696
1711
  if (method.skill) {
1697
1712
  args.push("--skill", method.skill);
1698
1713
  }
@@ -1767,7 +1782,7 @@ function detectVersion(method, spawn) {
1767
1782
  if (!npmRoot) return void 0;
1768
1783
  const pkgJson = join7(npmRoot, method.pkg, "package.json");
1769
1784
  if (!existsSync8(pkgJson)) return void 0;
1770
- const parsed = JSON.parse(readFileSync7(pkgJson, "utf8"));
1785
+ const parsed = JSON.parse(readFileSync8(pkgJson, "utf8"));
1771
1786
  return parsed.version;
1772
1787
  }
1773
1788
  default:
@@ -1795,7 +1810,7 @@ function getNpmGlobalRoot(spawn) {
1795
1810
  // src/install-log.ts
1796
1811
  init_esm_shims();
1797
1812
  import { createHash } from "crypto";
1798
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
1813
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
1799
1814
  import { dirname as dirname3, join as join8 } from "path";
1800
1815
  var INSTALL_LOG_FILENAME = ".harness-install.json";
1801
1816
  var INSTALL_LOG_VERSION = 1;
@@ -1864,7 +1879,7 @@ function readInstallLog(projectDir) {
1864
1879
  const path = join8(projectDir, ".claude", INSTALL_LOG_FILENAME);
1865
1880
  if (!existsSync9(path)) return null;
1866
1881
  try {
1867
- const parsed = JSON.parse(readFileSync8(path, "utf8"));
1882
+ const parsed = JSON.parse(readFileSync9(path, "utf8"));
1868
1883
  if (Array.isArray(parsed.assets)) {
1869
1884
  parsed.assets = parsed.assets.map(
1870
1885
  (a) => a.method === "npm-global" ? { ...a, method: "npm" } : a
@@ -2127,7 +2142,7 @@ function buildManifest(spec) {
2127
2142
 
2128
2143
  // src/mcp-merge.ts
2129
2144
  init_esm_shims();
2130
- import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
2145
+ import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
2131
2146
  function parseTrackMcpMap(raw) {
2132
2147
  const rows = [];
2133
2148
  for (const line of raw.split(/\r?\n/)) {
@@ -2178,15 +2193,15 @@ function mergeMcpServers(base, rows, tracks) {
2178
2193
  return out;
2179
2194
  }
2180
2195
  function composeMcpJson(opts) {
2181
- const base = JSON.parse(readFileSync9(opts.templateMcpPath, "utf8"));
2196
+ const base = JSON.parse(readFileSync10(opts.templateMcpPath, "utf8"));
2182
2197
  const merged = opts.existingPath && existsSync10(opts.existingPath) ? mergeUserBase(base, opts.existingPath) : base;
2183
- const mapRaw = existsSync10(opts.trackMapPath) ? readFileSync9(opts.trackMapPath, "utf8") : "";
2198
+ const mapRaw = existsSync10(opts.trackMapPath) ? readFileSync10(opts.trackMapPath, "utf8") : "";
2184
2199
  const rows = parseTrackMcpMap(mapRaw);
2185
2200
  return mergeMcpServers(merged, rows, opts.tracks);
2186
2201
  }
2187
2202
  function mergeUserBase(base, existingPath) {
2188
2203
  try {
2189
- const existing = JSON.parse(readFileSync9(existingPath, "utf8"));
2204
+ const existing = JSON.parse(readFileSync10(existingPath, "utf8"));
2190
2205
  return {
2191
2206
  ...base,
2192
2207
  mcpServers: { ...base.mcpServers, ...existing.mcpServers }
@@ -2202,7 +2217,7 @@ function writeMcpJson(path, mcp) {
2202
2217
 
2203
2218
  // src/opencode/transform.ts
2204
2219
  init_esm_shims();
2205
- import { copyFileSync as copyFileSync2, existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "fs";
2220
+ import { copyFileSync as copyFileSync2, existsSync as existsSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
2206
2221
  import { basename as basename3, join as join9 } from "path";
2207
2222
 
2208
2223
  // src/opencode/agents-md.ts
@@ -2315,7 +2330,7 @@ function runOpencodeTransform(params) {
2315
2330
  const cmdSrc = join9(harnessRoot, "templates/commands/uzys", `${phase}.md`);
2316
2331
  let source = "";
2317
2332
  if (existsSync11(cmdSrc)) {
2318
- source = readFileSync10(cmdSrc, "utf8");
2333
+ source = readFileSync11(cmdSrc, "utf8");
2319
2334
  } else {
2320
2335
  const fallback = join9(
2321
2336
  harnessRoot,
@@ -2323,7 +2338,7 @@ function runOpencodeTransform(params) {
2323
2338
  `uzys-${phase}.md`
2324
2339
  );
2325
2340
  if (existsSync11(fallback)) {
2326
- source = readFileSync10(fallback, "utf8");
2341
+ source = readFileSync11(fallback, "utf8");
2327
2342
  }
2328
2343
  }
2329
2344
  const target = join9(cmdDir, `uzys-${phase}.md`);
@@ -2345,14 +2360,14 @@ function readRequired2(path) {
2345
2360
  if (!existsSync11(path)) {
2346
2361
  throw new Error(`OpenCode transform: required source missing: ${path}`);
2347
2362
  }
2348
- return readFileSync10(path, "utf8");
2363
+ return readFileSync11(path, "utf8");
2349
2364
  }
2350
2365
  function readOptionalJson2(path) {
2351
2366
  if (!existsSync11(path)) {
2352
2367
  return null;
2353
2368
  }
2354
2369
  try {
2355
- return JSON.parse(readFileSync10(path, "utf8"));
2370
+ return JSON.parse(readFileSync11(path, "utf8"));
2356
2371
  } catch {
2357
2372
  return null;
2358
2373
  }
@@ -2360,7 +2375,7 @@ function readOptionalJson2(path) {
2360
2375
 
2361
2376
  // src/project-claude-merge.ts
2362
2377
  init_esm_shims();
2363
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
2378
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
2364
2379
  import { join as join10 } from "path";
2365
2380
  var SECTIONS = [
2366
2381
  "stack",
@@ -2411,7 +2426,7 @@ var FULL_EXPANSION = [
2411
2426
  ];
2412
2427
  function mergeProjectClaude(tracks, opts) {
2413
2428
  const expanded = expandTracks(tracks);
2414
- const baseRaw = readFileSync11(join10(opts.baseDir, "_base.md"), "utf8");
2429
+ const baseRaw = readFileSync12(join10(opts.baseDir, "_base.md"), "utf8");
2415
2430
  let output = baseRaw;
2416
2431
  output = output.replace("<!-- INSERT: track-list -->", trackList(expanded));
2417
2432
  output = output.replace("<!-- INSERT: tagline -->", taglineList(expanded, opts.baseDir));
@@ -2443,7 +2458,7 @@ function taglineList(tracks, baseDir) {
2443
2458
  if (!existsSync12(path)) {
2444
2459
  continue;
2445
2460
  }
2446
- const body = readFileSync11(path, "utf8").trim();
2461
+ const body = readFileSync12(path, "utf8").trim();
2447
2462
  if (body) {
2448
2463
  taglines.push(body);
2449
2464
  }
@@ -2457,7 +2472,7 @@ function renderSection(section, tracks, baseDir) {
2457
2472
  if (!existsSync12(path)) {
2458
2473
  continue;
2459
2474
  }
2460
- const body = readFileSync11(path, "utf8").trim();
2475
+ const body = readFileSync12(path, "utf8").trim();
2461
2476
  if (body) {
2462
2477
  present.push({ track: t, body });
2463
2478
  }
@@ -2516,7 +2531,7 @@ import {
2516
2531
  copyFileSync as copyFileSync3,
2517
2532
  existsSync as existsSync13,
2518
2533
  readdirSync as readdirSync3,
2519
- readFileSync as readFileSync12,
2534
+ readFileSync as readFileSync13,
2520
2535
  unlinkSync,
2521
2536
  writeFileSync as writeFileSync10
2522
2537
  } from "fs";
@@ -2605,7 +2620,7 @@ function pruneOrphans(target, source, ext) {
2605
2620
  function cleanStaleHookRefs(settingsPath, hooksDir) {
2606
2621
  let settings;
2607
2622
  try {
2608
- settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
2623
+ settings = JSON.parse(readFileSync13(settingsPath, "utf8"));
2609
2624
  } catch {
2610
2625
  return [];
2611
2626
  }
@@ -2671,7 +2686,8 @@ function runInstall(ctx) {
2671
2686
  mode,
2672
2687
  envFiles: writeEnvironmentFiles(projectDir, spec.tracks),
2673
2688
  categories: base.categories,
2674
- rootClaudeMd: base.rootClaudeMd
2689
+ rootClaudeMd: base.rootClaudeMd,
2690
+ backups: base.backups
2675
2691
  };
2676
2692
  ctx.onProgress?.({ type: "baseline-complete", baseline });
2677
2693
  const external = runExternalPhase(ctx);
@@ -2733,7 +2749,8 @@ function emptyClaudeBaseline() {
2733
2749
  skipped: 0,
2734
2750
  categories: { rules: [], agents: [], hooks: [], commands: 0, skills: [] },
2735
2751
  rootClaudeMd: null,
2736
- rootClaudeMdLog: null
2752
+ rootClaudeMdLog: null,
2753
+ backups: []
2737
2754
  };
2738
2755
  }
2739
2756
  function installClaudeBaseline(manifestSpec, harnessRoot, projectDir, templatesDir) {
@@ -2751,6 +2768,12 @@ function installClaudeBaseline(manifestSpec, harnessRoot, projectDir, templatesD
2751
2768
  continue;
2752
2769
  }
2753
2770
  if (entry.type === "file") {
2771
+ if (entry.target === ".claude/settings.json") {
2772
+ const backup = backupFileIfChanged(target, readFileSync14(source, "utf-8"));
2773
+ if (backup) {
2774
+ result.backups.push(backup);
2775
+ }
2776
+ }
2754
2777
  copyFile(source, target);
2755
2778
  result.filesCopied += 1;
2756
2779
  } else {
@@ -2764,9 +2787,12 @@ function installClaudeBaseline(manifestSpec, harnessRoot, projectDir, templatesD
2764
2787
  chmodHooksSync(hookDir);
2765
2788
  }
2766
2789
  writeInstalledTracks(projectDir, manifestSpec.tracks);
2767
- const rootClaudeMdContent = writeRootClaudeMd(harnessRoot, projectDir, manifestSpec.tracks);
2790
+ const rootClaudeMd = writeRootClaudeMd(harnessRoot, projectDir, manifestSpec.tracks);
2768
2791
  result.rootClaudeMd = { tracks: manifestSpec.tracks };
2769
- result.rootClaudeMdLog = { path: "CLAUDE.md", sha256: hashContent(rootClaudeMdContent) };
2792
+ result.rootClaudeMdLog = { path: "CLAUDE.md", sha256: hashContent(rootClaudeMd.content) };
2793
+ if (rootClaudeMd.backup) {
2794
+ result.backups.push(rootClaudeMd.backup);
2795
+ }
2770
2796
  return result;
2771
2797
  }
2772
2798
  function writeEnvironmentFiles(projectDir, tracks) {
@@ -2893,7 +2919,7 @@ function wireKarpathyHook(spec, external, harnessRoot, projectDir) {
2893
2919
  const settingsPath = join12(projectDir, ".claude/settings.json");
2894
2920
  let settingsUpdated = false;
2895
2921
  if (existsSync14(settingsPath)) {
2896
- const raw = readFileSync13(settingsPath, "utf8");
2922
+ const raw = readFileSync14(settingsPath, "utf8");
2897
2923
  let before;
2898
2924
  try {
2899
2925
  before = JSON.parse(raw);
@@ -2950,8 +2976,10 @@ function writeInstalledTracks(projectDir, tracks) {
2950
2976
  function writeRootClaudeMd(harnessRoot, projectDir, tracks) {
2951
2977
  const baseDir = join12(harnessRoot, "templates/project-claude");
2952
2978
  const content = mergeProjectClaude(tracks, { baseDir });
2953
- writeFileSync11(join12(projectDir, "CLAUDE.md"), content);
2954
- return content;
2979
+ const target = join12(projectDir, "CLAUDE.md");
2980
+ const backup = backupFileIfChanged(target, content);
2981
+ writeFileSync11(target, content);
2982
+ return { content, backup };
2955
2983
  }
2956
2984
  function chmodHooksSync(hookDir) {
2957
2985
  for (const file of listHookFiles(hookDir)) {
@@ -3223,7 +3251,15 @@ function renderFinalSummary(log, spec, report, fromWizard) {
3223
3251
  }
3224
3252
  }
3225
3253
  log("");
3226
- log(infoRow("NEXT", `${c.bold("claude")} \u2192 ${c.cyan("/uzys:spec")}`));
3254
+ const hasUzysHarness = isAssetSelected("uzys-harness", spec);
3255
+ const hasClaude = spec.cli.includes("claude");
3256
+ if (hasUzysHarness && hasClaude) {
3257
+ log(infoRow("NEXT", `${c.bold("claude")} \u2192 ${c.cyan("/uzys:spec")}`));
3258
+ } else {
3259
+ const primary = (hasClaude ? "claude" : spec.cli[0]) ?? "claude";
3260
+ const label = CLI_SUMMARY_LABELS[primary];
3261
+ log(infoRow("NEXT", `Open ${c.bold(label)} \u2014 installed rules & skills are now active`));
3262
+ }
3227
3263
  log("");
3228
3264
  }
3229
3265
  function formatAssetMeta(asset, version) {
@@ -3272,6 +3308,11 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false) {
3272
3308
  }
3273
3309
  return;
3274
3310
  }
3311
+ if (baseline.backups) {
3312
+ for (const b2 of baseline.backups) {
3313
+ log(assetRow("success", "backup", shortenPath(b2)));
3314
+ }
3315
+ }
3275
3316
  const cats = baseline.categories;
3276
3317
  if (cats) {
3277
3318
  const phase1Row = (label, count, useText, files) => {
@@ -3363,7 +3404,7 @@ function renderPhase1Rows(log, baseline, verbose = false, withEcc = false) {
3363
3404
  log(
3364
3405
  ` ${c.dim("\xB7")} ${c.dim("ECC plugin not selected \u2014 cherry-pick fallback active (up to 4 agents + 8 skills + 3 commands)")}`
3365
3406
  );
3366
- log(` ${c.dim("\xB7")} ${c.dim("Use --with-ecc to install ECC plugin instead")}`);
3407
+ log(` ${c.dim("\xB7")} ${c.dim("Use --with ecc-plugin to install ECC plugin instead")}`);
3367
3408
  }
3368
3409
  if (baseline.envFiles.envExampleCreated) {
3369
3410
  log(assetRow("success", ".env.example", "Supabase token guide"));
@@ -3591,11 +3632,9 @@ function registerInstallCommand(cli2) {
3591
3632
  }
3592
3633
  ).option("--project-dir <path>", "[Project] Target project directory", {
3593
3634
  default: process.cwd()
3635
+ }).option("--scope <scope>", "[Scope] Installation scope: project (default) | global", {
3636
+ default: "project"
3594
3637
  }).option(
3595
- "--scope <scope>",
3596
- "[Scope] Installation scope: project (default) | global. ADR-020 / NORTH_STAR D16",
3597
- { default: "project" }
3598
- ).option(
3599
3638
  "--with <asset-id>",
3600
3639
  "[Asset] Force-include External Asset id (regardless of preset). Repeatable. v26.47.0+"
3601
3640
  ).option(
@@ -3603,10 +3642,10 @@ function registerInstallCommand(cli2) {
3603
3642
  "[Asset] Force-exclude External Asset id (drop from preset recommendation). Repeatable. v26.47.0+"
3604
3643
  ).option(
3605
3644
  "--with-codex-prompts",
3606
- "[Codex] Unify Codex slash (~/.codex/prompts/uzys-*.md). v26.46.0+ default ON when --cli codex"
3645
+ "[Codex] Unify Codex slash (~/.codex/prompts/uzys-*.md). Requires --cli codex. (v26.46.0+)"
3607
3646
  ).option(
3608
3647
  "--no-codex-prompts",
3609
- "[Codex] Disable Codex slash default ON (skip global copy even with --cli codex)"
3648
+ "[Codex] Backward-compat noop \u2014 Codex slash is opt-in via --with-codex-prompts (v26.64.0 ADR-020)"
3610
3649
  ).option(
3611
3650
  "--with-codex-skills",
3612
3651
  "[Codex] Codex global opt-in: copy uzys-* skills to ~/.codex/skills/"
@@ -3628,7 +3667,7 @@ function registerInstallCommand(cli2) {
3628
3667
  // src/commands/uninstall.ts
3629
3668
  init_esm_shims();
3630
3669
  import { spawnSync as spawnSync2 } from "child_process";
3631
- import { existsSync as existsSync15, readFileSync as readFileSync14, rmSync } from "fs";
3670
+ import { existsSync as existsSync15, readFileSync as readFileSync15, rmSync } from "fs";
3632
3671
  import { join as join13, resolve as resolve3 } from "path";
3633
3672
  function uninstallAction(options, deps = {}) {
3634
3673
  const log = deps.log ?? console.log;
@@ -3757,7 +3796,7 @@ function buildProjectReverseStep(asset, spawn) {
3757
3796
  return {
3758
3797
  label: `npx skills remove ${source}`,
3759
3798
  execute: () => {
3760
- const r = spawn("npx", ["skills", "remove", source, "--yes"]);
3799
+ const r = spawn("npx", [skillsCliSpec(), "remove", source, "--yes"]);
3761
3800
  return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
3762
3801
  }
3763
3802
  };
@@ -3816,7 +3855,7 @@ function rootClaudeMdModified(log, projectDir) {
3816
3855
  if (!rootMd) return false;
3817
3856
  const path = join13(projectDir, rootMd.path);
3818
3857
  if (!existsSync15(path)) return false;
3819
- return hashContent(readFileSync14(path, "utf8")) !== rootMd.sha256;
3858
+ return hashContent(readFileSync15(path, "utf8")) !== rootMd.sha256;
3820
3859
  }
3821
3860
  function formatTemplateList(log) {
3822
3861
  const items = [log.templates.claudeDir];
@@ -5102,7 +5141,7 @@ Proceed?`,
5102
5141
 
5103
5142
  // src/state.ts
5104
5143
  init_esm_shims();
5105
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
5144
+ import { existsSync as existsSync16, readFileSync as readFileSync16 } from "fs";
5106
5145
  import { join as join14 } from "path";
5107
5146
  var META_FILE = ".claude/.installed-tracks";
5108
5147
  var LEGACY_SIGNATURES = [
@@ -5127,7 +5166,7 @@ function detectInstallState(projectDir) {
5127
5166
  return { state: "existing", tracks, source: "legacy", hasClaudeDir: true };
5128
5167
  }
5129
5168
  function readMetafile(path) {
5130
- const raw = readFileSync15(path, "utf8");
5169
+ const raw = readFileSync16(path, "utf8");
5131
5170
  const seen = /* @__PURE__ */ new Set();
5132
5171
  for (const line of raw.split(/\s+/)) {
5133
5172
  const trimmed = line.trim();