@uzysjung/agent-harness 26.119.0 → 26.125.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.ko.md +12 -0
  2. package/README.md +9 -0
  3. package/dist/{chunk-2VII24JD.js → chunk-ZSLMD5SP.js} +10 -4
  4. package/dist/{chunk-2VII24JD.js.map → chunk-ZSLMD5SP.js.map} +1 -1
  5. package/dist/index.js +737 -220
  6. package/dist/index.js.map +1 -1
  7. package/dist/trust-tier-drift.js +1 -1
  8. package/package.json +1 -1
  9. package/templates/hooks/session-start.sh +24 -3
  10. package/templates/hooks/spec-drift-check.sh +24 -1
  11. package/templates/rules/cli-development.md +16 -0
  12. package/templates/rules/git-policy.md +4 -0
  13. package/templates/rules/no-false-ship.md +55 -0
  14. package/templates/skills/continuous-learning-v2/SKILL.md +25 -29
  15. package/templates/skills/continuous-learning-v2/agents/observer-loop.sh +362 -0
  16. package/templates/skills/continuous-learning-v2/agents/observer.md +189 -0
  17. package/templates/skills/continuous-learning-v2/agents/session-guardian.sh +150 -0
  18. package/templates/skills/continuous-learning-v2/agents/start-observer.sh +252 -0
  19. package/templates/skills/continuous-learning-v2/hooks/observe.sh +189 -32
  20. package/templates/skills/continuous-learning-v2/scripts/detect-project.sh +113 -19
  21. package/templates/skills/continuous-learning-v2/scripts/instinct-cli.py +558 -28
  22. package/templates/skills/continuous-learning-v2/scripts/lib/homunculus-dir.sh +31 -0
  23. package/templates/skills/continuous-learning-v2/scripts/migrate-homunculus.sh +68 -0
  24. package/templates/skills/continuous-learning-v2/scripts/test_parse_instinct.py +1420 -0
  25. package/templates/skills/harness-health-audit/SKILL.md +14 -2
  26. package/templates/skills/harness-health-audit/scripts/observation-digest.mjs +212 -0
  27. package/templates/skills/model-orchestration/SKILL.md +7 -0
  28. package/templates/skills/multi-persona-review/SKILL.md +22 -0
  29. package/templates/skills/recurrence-prevention/SKILL.md +4 -0
  30. package/templates/skills/strategic-compact/SKILL.md +26 -12
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  residentCost,
28
28
  resolveScope,
29
29
  summarizeContextCost
30
- } from "./chunk-2VII24JD.js";
30
+ } from "./chunk-ZSLMD5SP.js";
31
31
 
32
32
  // node_modules/sisteransi/src/index.js
33
33
  var require_src = __commonJS({
@@ -712,7 +712,7 @@ var cac = (name = "") => new CAC(name);
712
712
  // package.json
713
713
  var package_default = {
714
714
  name: "@uzysjung/agent-harness",
715
- version: "26.119.0",
715
+ version: "26.125.0",
716
716
  description: "Curate vetted AI-coding skills & plugins by your tech stack \u2014 install only what you need, across Claude Code, Codex, OpenCode & Antigravity",
717
717
  type: "module",
718
718
  publishConfig: {
@@ -1751,7 +1751,13 @@ function methodDetail(method) {
1751
1751
  return { key: method.key };
1752
1752
  }
1753
1753
  }
1754
- function buildInstallLog(spec, external, scope, rootClaudeMd) {
1754
+ function buildInstallLog(spec, external, scope, rootClaudeMd, previous, claudeDirMovedAside = false, rootFiles = []) {
1755
+ const templates = {
1756
+ claudeDir: ".claude/",
1757
+ ...spec.cli.includes("codex") ? { codexDir: ".codex/" } : {},
1758
+ ...spec.cli.includes("opencode") ? { opencodeDir: ".opencode/" } : {},
1759
+ ...rootClaudeMd ? { rootClaudeMd } : {}
1760
+ };
1755
1761
  const log = {
1756
1762
  schemaVersion: INSTALL_LOG_VERSION,
1757
1763
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1760,16 +1766,63 @@ function buildInstallLog(spec, external, scope, rootClaudeMd) {
1760
1766
  tracks: spec.tracks,
1761
1767
  cli: spec.cli
1762
1768
  },
1763
- templates: {
1764
- claudeDir: ".claude/",
1765
- ...spec.cli.includes("codex") ? { codexDir: ".codex/" } : {},
1766
- ...spec.cli.includes("opencode") ? { opencodeDir: ".opencode/" } : {},
1767
- ...rootClaudeMd ? { rootClaudeMd } : {}
1768
- },
1769
- assets: buildAssetEntries(external, scope)
1769
+ // 이번 설치가 만든 항목이 이기고, 이번에 안 만든 항목은 이전 값을 그대로 둔다.
1770
+ // (예: claude 로 깔고 나중에 codex 만 추가 설치해도 root CLAUDE.md 기록이 살아남는다)
1771
+ templates: { ...previous?.templates, ...templates },
1772
+ assets: mergeAssets(
1773
+ claudeDirMovedAside ? previous?.assets?.filter(survivesClaudeDirRename) : previous?.assets,
1774
+ buildAssetEntries(external, scope)
1775
+ )
1770
1776
  };
1777
+ const mergedRootFiles = mergeRootFiles(previous?.rootFiles, rootFiles);
1778
+ if (mergedRootFiles.length > 0) log.rootFiles = mergedRootFiles;
1771
1779
  return log;
1772
1780
  }
1781
+ function mergeRootFiles(previous, current) {
1782
+ const byPath = /* @__PURE__ */ new Map();
1783
+ for (const file of [...previous ?? [], ...current]) {
1784
+ const prior = byPath.get(file.path);
1785
+ byPath.set(
1786
+ file.path,
1787
+ prior ? {
1788
+ path: file.path,
1789
+ change: prior.change === "created" ? "created" : file.change,
1790
+ notes: [.../* @__PURE__ */ new Set([...prior.notes, ...file.notes])]
1791
+ } : file
1792
+ );
1793
+ }
1794
+ return [...byPath.values()];
1795
+ }
1796
+ function survivesClaudeDirRename(asset) {
1797
+ if (asset.scope === "global") return true;
1798
+ switch (asset.method) {
1799
+ case "plugin":
1800
+ return true;
1801
+ // `~/.claude/plugins/cache` — 프로젝트 밖
1802
+ case "npm":
1803
+ return true;
1804
+ // `node_modules/`
1805
+ case "skill":
1806
+ return false;
1807
+ // `npx skills add` project scope → `.claude/skills/`
1808
+ case "shell-script":
1809
+ return false;
1810
+ // ecc-prune → `.claude/local-plugins/`
1811
+ case "npx-run":
1812
+ return false;
1813
+ case "internal":
1814
+ return false;
1815
+ }
1816
+ }
1817
+ function mergeAssets(previous, current) {
1818
+ if (!previous || previous.length === 0) return [...current];
1819
+ const currentById = new Map(current.map((a) => [a.id, a]));
1820
+ const previousIds = new Set(previous.map((a) => a.id));
1821
+ return [
1822
+ ...previous.map((a) => currentById.get(a.id) ?? a),
1823
+ ...current.filter((a) => !previousIds.has(a.id))
1824
+ ];
1825
+ }
1773
1826
  function hashContent(content) {
1774
1827
  return createHash("sha256").update(content, "utf8").digest("hex");
1775
1828
  }
@@ -2185,7 +2238,8 @@ function keepHookRef(hook, hooksDir, removed) {
2185
2238
  }
2186
2239
 
2187
2240
  // src/installer.ts
2188
- var KARPATHY_HOOK_COMMAND = 'bash "$CLAUDE_PROJECT_DIR/.claude/hooks/karpathy-gate.sh"';
2241
+ var KARPATHY_HOOK_RELPATH = ".claude/hooks/karpathy-gate.sh";
2242
+ var KARPATHY_HOOK_COMMAND = `bash "$CLAUDE_PROJECT_DIR/${KARPATHY_HOOK_RELPATH}"`;
2189
2243
  var KARPATHY_ASSET_ID = "karpathy-coder";
2190
2244
  function runInstall(ctx) {
2191
2245
  const { harnessRoot, projectDir, spec } = ctx;
@@ -2198,6 +2252,7 @@ function runInstall(ctx) {
2198
2252
  if (mode === "update" && !existsSync12(claudeDir)) {
2199
2253
  throw new Error(`Update mode requires existing .claude/ at ${claudeDir}`);
2200
2254
  }
2255
+ const previousLog = readInstallLog(projectDir);
2201
2256
  const backupPath = resolveBackupPath(ctx, mode, claudeDir);
2202
2257
  if (mode === "update") {
2203
2258
  return runUpdateInstall(ctx, templatesDir, backupPath);
@@ -2229,7 +2284,14 @@ function runInstall(ctx) {
2229
2284
  ctx.onProgress?.({ type: "baseline-complete", baseline });
2230
2285
  const external = runExternalPhase(ctx);
2231
2286
  const karpathyHook = wireKarpathyHook(spec, external, harnessRoot, projectDir);
2232
- writeInstallLogSafe(ctx, external, base.rootClaudeMdLog);
2287
+ writeInstallLogSafe(
2288
+ ctx,
2289
+ external,
2290
+ base.rootClaudeMdLog,
2291
+ previousLog,
2292
+ backupPath !== null,
2293
+ collectRootFiles(baseline.envFiles, ciScaffold, mcpResult.created)
2294
+ );
2233
2295
  return { ...baseline, external, karpathyHook };
2234
2296
  }
2235
2297
  function resolveBackupPath(ctx, mode, claudeDir) {
@@ -2411,9 +2473,17 @@ function runExternalPhase(ctx) {
2411
2473
  ctx.onProgress?.({ type: "external-complete", report: external });
2412
2474
  return external;
2413
2475
  }
2414
- function writeInstallLogSafe(ctx, external, rootClaudeMdLog) {
2476
+ function writeInstallLogSafe(ctx, external, rootClaudeMdLog, previousLog, claudeDirMovedAside, rootFiles) {
2415
2477
  try {
2416
- const log = buildInstallLog(ctx.spec, external, resolveScope(ctx.spec.scope), rootClaudeMdLog);
2478
+ const log = buildInstallLog(
2479
+ ctx.spec,
2480
+ external,
2481
+ resolveScope(ctx.spec.scope),
2482
+ rootClaudeMdLog,
2483
+ previousLog,
2484
+ claudeDirMovedAside,
2485
+ rootFiles
2486
+ );
2417
2487
  writeInstallLog(ctx.projectDir, log);
2418
2488
  } catch (e2) {
2419
2489
  ctx.onProgress?.({
@@ -2437,7 +2507,7 @@ function wireKarpathyHook(spec, external, harnessRoot, projectDir) {
2437
2507
  return { wired: false, reason: "plugin-install-failed" };
2438
2508
  }
2439
2509
  const sourceHook = join11(harnessRoot, "templates/hooks/karpathy-gate.sh");
2440
- const targetHook = join11(projectDir, ".claude/hooks/karpathy-gate.sh");
2510
+ const targetHook = join11(projectDir, KARPATHY_HOOK_RELPATH);
2441
2511
  let hookScriptCopied = false;
2442
2512
  if (existsSync12(sourceHook)) {
2443
2513
  copyFile(sourceHook, targetHook);
@@ -2470,6 +2540,7 @@ function wireKarpathyHook(spec, external, harnessRoot, projectDir) {
2470
2540
  }
2471
2541
  function composeAndWriteMcp(harnessRoot, projectDir, spec) {
2472
2542
  const mcpPath = join11(projectDir, ".mcp.json");
2543
+ const created = !existsSync12(mcpPath);
2473
2544
  const composed = composeMcpJson({
2474
2545
  templateMcpPath: join11(harnessRoot, "templates/mcp.json"),
2475
2546
  trackMapPath: join11(harnessRoot, "templates/track-mcp-map.tsv"),
@@ -2477,7 +2548,41 @@ function composeAndWriteMcp(harnessRoot, projectDir, spec) {
2477
2548
  tracks: spec.tracks
2478
2549
  });
2479
2550
  writeMcpJson(mcpPath, composed);
2480
- return composed;
2551
+ return { ...composed, created };
2552
+ }
2553
+ function collectRootFiles(envFiles, ciScaffold, mcpCreated) {
2554
+ const files = [
2555
+ {
2556
+ path: ".mcp.json",
2557
+ change: mcpCreated ? "created" : "modified",
2558
+ notes: [mcpCreated ? "MCP \uC11C\uBC84 \uC815\uC758 \uC0DD\uC131" : "MCP \uC11C\uBC84 \uC815\uC758 \uBCD1\uD569 (\uAE30\uC874 \uD56D\uBAA9 \uBCF4\uC874)"]
2559
+ }
2560
+ ];
2561
+ if (envFiles.envExampleCreated) {
2562
+ files.push({ path: ".env.example", change: "created", notes: ["Supabase \uD1A0\uD070 \uAC00\uC774\uB4DC"] });
2563
+ }
2564
+ if (envFiles.mcpAllowlist && envFiles.mcpAllowlist.length > 0) {
2565
+ files.push({
2566
+ path: ".mcp-allowlist",
2567
+ change: "created",
2568
+ notes: [`MCP allowlist (${envFiles.mcpAllowlist.length} server)`]
2569
+ });
2570
+ }
2571
+ const gitignoreAdded = [
2572
+ ...envFiles.gitignoreEnvAdded ? [".env"] : [],
2573
+ ...envFiles.gitignoreNpxSkillsAdded
2574
+ ];
2575
+ if (gitignoreAdded.length > 0) {
2576
+ files.push({
2577
+ path: ".gitignore",
2578
+ change: "modified",
2579
+ notes: [`\uCD94\uAC00\uB41C \uC904: ${gitignoreAdded.join(", ")}`]
2580
+ });
2581
+ }
2582
+ for (const workflow of ciScaffold?.written ?? []) {
2583
+ files.push({ path: workflow, change: "created", notes: ["CI \uC6CC\uD06C\uD50C\uB85C \uC2A4\uCE90\uD3F4\uB4DC"] });
2584
+ }
2585
+ return files;
2481
2586
  }
2482
2587
  function accumulateCategory(cats, entry) {
2483
2588
  const target = entry.target;
@@ -3169,228 +3274,102 @@ function registerInstallCommand(cli2) {
3169
3274
  ).option("--verbose", "[Misc] Show installed file lists per category (default: counts only)").example("install --track tooling --with karpathy-coder").example("install --track csr-supabase --cli claude --cli codex").example("install --track csr-supabase --without netlify-cli --with railway-skills").action((options) => installAction(options));
3170
3275
  }
3171
3276
 
3172
- // src/commands/uninstall.ts
3277
+ // src/commands/list.ts
3173
3278
  init_esm_shims();
3174
- import { spawnSync as spawnSync2 } from "child_process";
3175
- import { existsSync as existsSync13, readFileSync as readFileSync12, rmSync } from "fs";
3279
+ import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
3176
3280
  import { join as join12, resolve as resolve3 } from "path";
3177
- function uninstallAction(options, deps = {}) {
3281
+ function listAction(options = {}, deps = {}) {
3178
3282
  const log = deps.log ?? console.log;
3179
3283
  const err = deps.err ?? console.error;
3180
3284
  const exit = deps.exit ?? ((code) => process.exit(code));
3181
- const spawn = deps.spawn ?? defaultSpawn2;
3182
- const rm = deps.rm ?? defaultRm;
3183
3285
  const projectDir = resolve3(options.projectDir ?? process.cwd());
3184
3286
  const installLog = readInstallLog(projectDir);
3185
3287
  if (!installLog) {
3186
3288
  err(status.failure(c.red(`ERROR: install log not found at ${installLogPath(projectDir)}`)));
3187
- err(c.dim(" Was this project installed by agent-harness? Nothing to uninstall."));
3289
+ err(c.dim(" Nothing installed here by agent-harness."));
3188
3290
  exit(1);
3189
3291
  return;
3190
3292
  }
3191
- const { reverseSteps, globalAdvisories } = planReverse(installLog, spawn, rm, projectDir);
3192
3293
  log("");
3193
- log(c.bold("uzys-agent-harness \xB7 uninstall"));
3294
+ log(c.bold("uzys-agent-harness \xB7 installed"));
3194
3295
  log("");
3195
3296
  log(c.dim(` installed: ${installLog.installedAt}`));
3196
3297
  log(c.dim(` scope: ${installLog.scope}`));
3197
- log(c.dim(` assets: ${installLog.assets.length}`));
3298
+ log(c.dim(` tracks: ${installLog.spec.tracks.join(", ") || "(none)"}`));
3299
+ log(c.dim(` cli: ${installLog.spec.cli.join(", ") || "(none)"}`));
3198
3300
  log("");
3199
- if (options.dryRun) {
3200
- log(c.yellow("[DRY RUN] reverse list (\uC2E4\uC81C \uBCC0\uACBD \uC5C6\uC74C):"));
3201
- log("");
3202
- if (reverseSteps.length === 0) {
3203
- log(c.dim(" (no project-scope assets to reverse)"));
3204
- }
3205
- for (const step of reverseSteps) {
3206
- log(` \u25CB ${step.label}`);
3207
- }
3208
- if (!options.keepTemplates) {
3209
- log(` \u25CB remove templates: ${formatTemplateList(installLog)}`);
3210
- if (installLog.templates.rootClaudeMd) {
3211
- log(
3212
- rootClaudeMdModified(installLog, projectDir) ? " \u25CB keep CLAUDE.md (modified since install \u2014 preserved)" : " \u25CB remove CLAUDE.md"
3213
- );
3214
- }
3215
- }
3216
- if (globalAdvisories.length > 0) {
3217
- log("");
3218
- log(
3219
- c.yellow(
3220
- `[GLOBAL] ${globalAdvisories.length} asset(s) at scope=global \u2014 manual removal required (D16):`
3221
- )
3222
- );
3223
- for (const adv of globalAdvisories) {
3224
- log(c.dim(` \xB7 ${adv.asset.id} (${adv.asset.method}) \u2192 ${adv.command}`));
3225
- }
3226
- }
3227
- log("");
3228
- exit(0);
3229
- return;
3230
- }
3231
- let succeeded = 0;
3232
- let failed = 0;
3233
- for (const step of reverseSteps) {
3234
- const result = step.execute();
3235
- if (result.ok) {
3236
- log(` ${status.success("\u2713")} ${step.label}`);
3237
- succeeded++;
3238
- } else {
3239
- log(` ${c.yellow("\u2298")} ${step.label} (${result.message ?? "failed"})`);
3240
- failed++;
3241
- }
3301
+ log(c.bold(` Assets (${installLog.assets.length})`));
3302
+ if (installLog.assets.length === 0) {
3303
+ log(c.dim(" (none \u2014 \uB0B4\uBD80 \uD15C\uD50C\uB9BF\uB9CC \uC124\uCE58\uB428)"));
3242
3304
  }
3243
- if (!options.keepTemplates) {
3244
- const { rootClaudeMdKept } = removeTemplates(installLog, projectDir, rm);
3245
- log(` ${status.success("\u2713")} templates removed: ${formatTemplateList(installLog)}`);
3246
- if (rootClaudeMdKept) {
3247
- log(
3248
- ` ${c.yellow("\u2298")} CLAUDE.md kept \u2014 modified since install. Remove manually if intended.`
3249
- );
3250
- }
3305
+ for (const line of formatAssetRows(installLog.assets)) {
3306
+ log(line);
3251
3307
  }
3252
- if (options.keepTemplates) {
3253
- rm(installLogPath(projectDir));
3254
- log(` ${status.success("\u2713")} install log removed (templates kept)`);
3308
+ log("");
3309
+ log(c.bold(" Templates"));
3310
+ for (const line of formatTemplateRows(installLog, projectDir)) {
3311
+ log(line);
3255
3312
  }
3256
- if (globalAdvisories.length > 0) {
3313
+ const rootRows = formatRootFileRows(installLog.rootFiles ?? [], projectDir);
3314
+ if (rootRows.length > 0) {
3257
3315
  log("");
3258
- log(
3259
- c.yellow(
3260
- `[GLOBAL] ${globalAdvisories.length} asset(s) at scope=global \u2014 manual removal required (D16):`
3261
- )
3262
- );
3263
- for (const adv of globalAdvisories) {
3264
- log(c.dim(` \xB7 ${adv.asset.id} (${adv.asset.method})`));
3265
- log(c.dim(` ${adv.command}`));
3266
- }
3316
+ log(c.bold(" Root files"));
3317
+ log(c.dim(" (uninstall \uC774 \uC9C0\uC6B0\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uC0AC\uC6A9\uC790 \uB0B4\uC6A9\uC774 \uC11E\uC778\uB2E4)"));
3318
+ for (const line of rootRows) log(line);
3267
3319
  }
3268
3320
  log("");
3269
- log(
3270
- succeeded === reverseSteps.length && failed === 0 ? status.success(c.green(`uninstall complete (${succeeded} asset(s))`)) : c.yellow(`uninstall finished with ${failed} skip(s) (${succeeded} ok)`)
3271
- );
3272
- exit(failed === 0 ? 0 : 1);
3273
- }
3274
- function planReverse(log, spawn, _rm, _projectDir) {
3275
- const reverseSteps = [];
3276
- const globalAdvisories = [];
3277
- for (const asset of log.assets) {
3278
- if (asset.scope === "global") {
3279
- globalAdvisories.push({ asset, command: buildGlobalAdvisoryCmd(asset) });
3280
- continue;
3281
- }
3282
- const step = buildProjectReverseStep(asset, spawn);
3283
- if (step) reverseSteps.push(step);
3284
- }
3285
- return { reverseSteps, globalAdvisories };
3286
- }
3287
- function buildProjectReverseStep(asset, spawn) {
3288
- switch (asset.method) {
3289
- case "plugin": {
3290
- const pluginId = asset.detail.pluginId ?? asset.id;
3291
- return {
3292
- label: `claude plugin uninstall --scope project ${pluginId}`,
3293
- execute: () => {
3294
- const r = spawn("claude", ["plugin", "uninstall", "--scope", "project", pluginId]);
3295
- return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
3296
- }
3297
- };
3298
- }
3299
- case "skill": {
3300
- const source = asset.detail.source ?? asset.id;
3301
- return {
3302
- label: `npx skills remove ${source}`,
3303
- execute: () => {
3304
- const r = spawn("npx", [skillsCliSpec(), "remove", source, "--yes"]);
3305
- return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
3306
- }
3307
- };
3308
- }
3309
- case "npm": {
3310
- const pkg = asset.detail.pkg ?? asset.id;
3311
- return {
3312
- label: `npm uninstall --save-dev ${pkg}`,
3313
- execute: () => {
3314
- const r = spawn("npm", ["uninstall", "--save-dev", pkg]);
3315
- return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
3316
- }
3317
- };
3318
- }
3319
- case "npx-run":
3320
- return null;
3321
- case "shell-script":
3322
- return null;
3323
- case "internal":
3324
- return null;
3325
- }
3326
- }
3327
- function buildGlobalAdvisoryCmd(asset) {
3328
- switch (asset.method) {
3329
- case "plugin": {
3330
- const pid = asset.detail.pluginId ?? asset.id;
3331
- return `claude plugin uninstall --scope user ${pid}`;
3332
- }
3333
- case "skill": {
3334
- const s = asset.detail.source ?? asset.id;
3335
- return `npx skills remove -g ${s}`;
3336
- }
3337
- case "npm": {
3338
- const pkg = asset.detail.pkg ?? asset.id;
3339
- return `npm uninstall -g ${pkg}`;
3340
- }
3341
- case "npx-run":
3342
- case "shell-script":
3343
- case "internal":
3344
- return "(no standard reverse \u2014 manual)";
3345
- }
3321
+ log(c.dim(" remove one: agent-harness uninstall --only <id>"));
3322
+ log(c.dim(" remove all: agent-harness uninstall"));
3323
+ log("");
3324
+ exit(0);
3325
+ }
3326
+ function formatAssetRows(assets) {
3327
+ const idWidth = Math.max(0, ...assets.map((a) => a.id.length));
3328
+ const methodWidth = Math.max(0, ...assets.map((a) => a.method.length));
3329
+ return assets.map((a) => {
3330
+ const marker = a.scope === "global" ? c.yellow("!") : c.green("\u2713");
3331
+ const version = a.version ? c.dim(` v${a.version}`) : "";
3332
+ const note = a.scope === "global" ? c.yellow(" (manual removal \u2014 D16)") : "";
3333
+ return ` ${marker} ${padDisplay(a.id, idWidth)} ${c.dim(padDisplay(a.method, methodWidth))} ${c.dim(a.scope)}${version}${note}`;
3334
+ });
3346
3335
  }
3347
- function removeTemplates(log, projectDir, rm) {
3348
- rm(join12(projectDir, log.templates.claudeDir));
3349
- if (log.templates.codexDir) rm(join12(projectDir, log.templates.codexDir));
3350
- if (log.templates.opencodeDir) rm(join12(projectDir, log.templates.opencodeDir));
3336
+ function formatTemplateRows(log, projectDir) {
3337
+ const dirs = [log.templates.claudeDir, log.templates.codexDir, log.templates.opencodeDir].filter(
3338
+ (d2) => Boolean(d2)
3339
+ );
3340
+ const rows = [` ${c.dim(dirs.join(" "))}`];
3351
3341
  const rootMd = log.templates.rootClaudeMd;
3352
3342
  if (rootMd) {
3353
- if (rootClaudeMdModified(log, projectDir)) return { rootClaudeMdKept: true };
3354
- rm(join12(projectDir, rootMd.path));
3343
+ const path = join12(projectDir, rootMd.path);
3344
+ const modified = existsSync13(path) && hashContent(readFileSync12(path, "utf8")) !== rootMd.sha256;
3345
+ rows.push(
3346
+ modified ? ` ${c.dim(rootMd.path)} ${c.yellow("(\uC218\uC815\uB428 \u2014 uninstall \uC2DC \uBCF4\uC874)")}` : ` ${c.dim(rootMd.path)}`
3347
+ );
3355
3348
  }
3356
- return { rootClaudeMdKept: false };
3357
- }
3358
- function rootClaudeMdModified(log, projectDir) {
3359
- const rootMd = log.templates.rootClaudeMd;
3360
- if (!rootMd) return false;
3361
- const path = join12(projectDir, rootMd.path);
3362
- if (!existsSync13(path)) return false;
3363
- return hashContent(readFileSync12(path, "utf8")) !== rootMd.sha256;
3364
- }
3365
- function formatTemplateList(log) {
3366
- const items = [log.templates.claudeDir];
3367
- if (log.templates.codexDir) items.push(log.templates.codexDir);
3368
- if (log.templates.opencodeDir) items.push(log.templates.opencodeDir);
3369
- return items.join(", ");
3370
- }
3371
- function defaultSpawn2(cmd, args) {
3372
- return spawnSync2(cmd, [...args], { encoding: "utf8", stdio: "pipe", timeout: 12e4 });
3349
+ return rows;
3373
3350
  }
3374
- function defaultRm(path) {
3375
- if (existsSync13(path)) {
3376
- rmSync(path, { recursive: true, force: true });
3377
- }
3351
+ function formatRootFileRows(rootFiles, projectDir) {
3352
+ const present = rootFiles.filter((f) => existsSync13(join12(projectDir, f.path)));
3353
+ const width = Math.max(0, ...present.map((f) => f.path.length));
3354
+ return present.map(
3355
+ (f) => ` ${c.dim(padDisplay(f.path, width))} ${c.dim(f.change === "created" ? "\uC0DD\uC131" : "\uBCD1\uD569")} ${c.dim(f.notes.join(" / "))}`
3356
+ );
3378
3357
  }
3379
- function registerUninstallCommand(cli2) {
3380
- cli2.command("uninstall", "Uninstall harness assets (log-based reverse)").option("--project-dir <path>", "[Project] Target project directory", {
3358
+ function registerListCommand(cli2) {
3359
+ cli2.command("list", "Show what agent-harness installed in this project").option("--project-dir <path>", "[Project] Target project directory", {
3381
3360
  default: process.cwd()
3382
- }).option("--dry-run", "[Mode] List reverse steps without executing").option(
3383
- "--keep-templates",
3384
- "[Mode] Keep `.claude/`, `.codex/`, `.opencode/` templates (remove only external assets)"
3385
- ).action((options) => {
3386
- uninstallAction(options);
3361
+ }).action((options) => {
3362
+ listAction(options);
3387
3363
  });
3388
3364
  }
3389
3365
 
3390
- // src/interactive.ts
3366
+ // src/commands/uninstall.ts
3391
3367
  init_esm_shims();
3368
+ import { spawnSync as spawnSync2 } from "child_process";
3369
+ import { existsSync as existsSync14, readFileSync as readFileSync13, rmSync } from "fs";
3370
+ import { join as join13, resolve as resolve4 } from "path";
3392
3371
 
3393
- // src/prompts.ts
3372
+ // src/uninstall-interactive.ts
3394
3373
  init_esm_shims();
3395
3374
 
3396
3375
  // node_modules/@clack/prompts/dist/index.mjs
@@ -4363,6 +4342,521 @@ ${a}
4363
4342
  };
4364
4343
  var jt = `${e("gray", $)} `;
4365
4344
 
4345
+ // src/uninstall-interactive.ts
4346
+ function buildRemovableRows(assets) {
4347
+ return assets.map((a) => ({
4348
+ value: a.id,
4349
+ label: `${a.id} [${a.method}]${a.version ? ` v${a.version}` : ""}`,
4350
+ hint: hintFor(a)
4351
+ }));
4352
+ }
4353
+ function hintFor(asset) {
4354
+ if (asset.scope === "global") return "global scope \u2014 \uC790\uB3D9 \uC0AD\uC81C \uC548 \uD568, \uC218\uAE30 \uC81C\uAC70 \uBA85\uB839\uC744 \uCD9C\uB825\uD55C\uB2E4";
4355
+ switch (asset.method) {
4356
+ case "plugin":
4357
+ return "claude plugin uninstall --scope project";
4358
+ case "skill":
4359
+ return "npx skills remove";
4360
+ case "npm":
4361
+ return "npm uninstall --save-dev";
4362
+ case "npx-run":
4363
+ case "shell-script":
4364
+ case "internal":
4365
+ return "\uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C \u2014 \uC804\uB7C9 \uC81C\uAC70(`.claude/` \uC0AD\uC81C)\uB85C\uB9CC \uC0AC\uB77C\uC9C4\uB2E4";
4366
+ }
4367
+ }
4368
+ async function runInteractiveUninstall(projectDir, deps = {}) {
4369
+ const prompts = deps.prompts ?? defaultUninstallPrompts();
4370
+ const isTty = deps.isTty ?? (() => Boolean(process.stdin.isTTY));
4371
+ const readLog = deps.readLog ?? readInstallLog;
4372
+ if (!isTty()) return { ok: false, reason: "no-tty" };
4373
+ const log = readLog(projectDir);
4374
+ if (!log) return { ok: false, reason: "no-log" };
4375
+ prompts.intro("uzys-agent-harness \xB7 uninstall");
4376
+ const rows = buildRemovableRows(log.assets);
4377
+ const mode = await prompts.selectMode(rows.length);
4378
+ if (mode === null) {
4379
+ prompts.cancel("Cancelled.");
4380
+ return { ok: false, reason: "cancelled" };
4381
+ }
4382
+ if (mode === "all") {
4383
+ const ok2 = await prompts.confirm(
4384
+ [
4385
+ "\uC804\uB7C9 \uC81C\uAC70 \u2014 \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uB2E4:",
4386
+ ` \xB7 \uC790\uC0B0 ${log.assets.length}\uAC1C (\uC790\uB3D9 \uACBD\uB85C\uAC00 \uC788\uB294 \uAC83\uB9CC \uC2E4\uC81C \uC81C\uAC70)`,
4387
+ " \xB7 templates \uC0AD\uC81C: `.claude/` \uB4F1 (\uC124\uCE58 \uAE30\uB85D\uB3C4 \uD568\uAED8 \uC0AC\uB77C\uC9C4\uB2E4)",
4388
+ " \xB7 `.claude/` \uBC16 \uD30C\uC77C(`.mcp.json` \uB4F1)\uC740 \uC0AD\uC81C\uD558\uC9C0 \uC54A\uACE0 \uC548\uB0B4\uB9CC \uD55C\uB2E4"
4389
+ ].join("\n")
4390
+ );
4391
+ if (!ok2) {
4392
+ prompts.cancel("Cancelled.");
4393
+ return { ok: false, reason: "cancelled" };
4394
+ }
4395
+ return { ok: true, options: { projectDir } };
4396
+ }
4397
+ const picked = await prompts.selectAssets(rows);
4398
+ if (picked === null) {
4399
+ prompts.cancel("Cancelled.");
4400
+ return { ok: false, reason: "cancelled" };
4401
+ }
4402
+ if (picked.length === 0) {
4403
+ prompts.outro("\uC544\uBB34\uAC83\uB3C4 \uC120\uD0DD\uD558\uC9C0 \uC54A\uC558\uB2E4 \u2014 \uBCC0\uACBD \uC5C6\uC74C.");
4404
+ return { ok: false, reason: "nothing-selected" };
4405
+ }
4406
+ const ok = await prompts.confirm(
4407
+ [
4408
+ `\uC120\uD0DD \uC81C\uAC70 (${picked.length}\uAC1C):`,
4409
+ ...picked.map((id) => ` \xB7 ${id}`),
4410
+ "",
4411
+ "templates \uB294 \uADF8\uB300\uB85C \uB454\uB2E4."
4412
+ ].join("\n")
4413
+ );
4414
+ if (!ok) {
4415
+ prompts.cancel("Cancelled.");
4416
+ return { ok: false, reason: "cancelled" };
4417
+ }
4418
+ return { ok: true, options: { projectDir, only: picked.join(",") } };
4419
+ }
4420
+ function defaultUninstallPrompts() {
4421
+ return {
4422
+ intro: (m) => ge(m),
4423
+ outro: (m) => ye(m),
4424
+ cancel: (m) => me(m),
4425
+ selectMode: async (rowCount) => {
4426
+ const r = await Ee({
4427
+ message: `\uBB34\uC5C7\uC744 \uC81C\uAC70\uD560\uAE4C? (\uC124\uCE58\uB41C \uC790\uC0B0 ${rowCount}\uAC1C)`,
4428
+ options: [
4429
+ {
4430
+ value: "selected",
4431
+ label: "\uD56D\uBAA9 \uC120\uD0DD\uD574\uC11C \uC81C\uAC70",
4432
+ hint: "templates(`.claude/` \uB4F1)\uB294 \uADF8\uB300\uB85C \uB454\uB2E4"
4433
+ },
4434
+ {
4435
+ value: "all",
4436
+ label: "\uC804\uBD80 \uC81C\uAC70",
4437
+ hint: "\uC790\uC0B0 + templates. \uC124\uCE58 \uAE30\uB85D\uB3C4 \uC0AC\uB77C\uC9C4\uB2E4"
4438
+ }
4439
+ ]
4440
+ });
4441
+ return q(r) ? null : r;
4442
+ },
4443
+ selectAssets: async (rows) => {
4444
+ if (rows.length === 0) return [];
4445
+ const r = await ve({
4446
+ message: "\uC81C\uAC70\uD560 \uD56D\uBAA9 (Space \uD1A0\uAE00 \xB7 Enter \uD655\uC815 \xB7 ESC \uCDE8\uC18C)",
4447
+ options: rows.map((x) => ({ value: x.value, label: x.label, hint: x.hint })),
4448
+ required: false
4449
+ });
4450
+ return q(r) ? null : r;
4451
+ },
4452
+ confirm: async (summary) => {
4453
+ const r = await ue({ message: `${summary}
4454
+
4455
+ \uC9C4\uD589\uD560\uAE4C?`, initialValue: false });
4456
+ return q(r) ? null : r;
4457
+ }
4458
+ };
4459
+ }
4460
+
4461
+ // src/commands/uninstall.ts
4462
+ function uninstallAction(options, deps = {}) {
4463
+ const log = deps.log ?? console.log;
4464
+ const err = deps.err ?? console.error;
4465
+ const exit = deps.exit ?? ((code) => process.exit(code));
4466
+ const spawn = deps.spawn ?? defaultSpawn2;
4467
+ const rm = deps.rm ?? defaultRm;
4468
+ const writeLog = deps.writeLog ?? writeInstallLog;
4469
+ const projectDir = resolve4(options.projectDir ?? process.cwd());
4470
+ const installLog = readInstallLog(projectDir);
4471
+ if (!installLog) {
4472
+ err(status.failure(c.red(`ERROR: install log not found at ${installLogPath(projectDir)}`)));
4473
+ err(c.dim(" Was this project installed by agent-harness? Nothing to uninstall."));
4474
+ exit(1);
4475
+ return;
4476
+ }
4477
+ const selectedIds = parseOnly(options.only);
4478
+ if (options.only !== void 0 && selectedIds === null) {
4479
+ err(status.failure(c.red("ERROR: --only \uC5D0 \uC790\uC0B0 id \uAC00 \uC5C6\uB2E4")));
4480
+ err(c.dim(" \uC608: --only code-review (id \uB294 `agent-harness list` \uC5D0\uC11C \uD655\uC778)"));
4481
+ exit(1);
4482
+ return;
4483
+ }
4484
+ const unknown = selectedIds ? unknownIds(installLog, selectedIds) : [];
4485
+ if (unknown.length > 0) {
4486
+ err(status.failure(c.red(`ERROR: not in install log: ${unknown.join(", ")}`)));
4487
+ err(c.dim(` installed: ${installLog.assets.map((a) => a.id).join(", ") || "(none)"}`));
4488
+ exit(1);
4489
+ return;
4490
+ }
4491
+ const targetAssets = selectedIds ? installLog.assets.filter((a) => selectedIds.includes(a.id)) : installLog.assets;
4492
+ const keepTemplates = options.keepTemplates || selectedIds !== null;
4493
+ const rootFiles = selectedIds ? [] : installLog.rootFiles ?? [];
4494
+ const plan = planReverse(targetAssets, spawn);
4495
+ for (const line of headerLines(installLog, selectedIds, targetAssets.length)) log(line);
4496
+ if (options.dryRun) {
4497
+ for (const line of dryRunLines(
4498
+ plan,
4499
+ installLog,
4500
+ projectDir,
4501
+ keepTemplates,
4502
+ targetAssets,
4503
+ rootFiles
4504
+ )) {
4505
+ log(line);
4506
+ }
4507
+ exit(0);
4508
+ return;
4509
+ }
4510
+ const logSurvives = selectedIds !== null;
4511
+ const { succeeded, failed, removedIds } = executeReverse(plan, log, logSurvives);
4512
+ if (!keepTemplates) {
4513
+ const { rootClaudeMdKept } = removeTemplates(installLog, projectDir, rm);
4514
+ log(` ${status.success(`templates removed: ${formatTemplateList(installLog)}`)}`);
4515
+ if (rootClaudeMdKept) {
4516
+ log(
4517
+ ` ${c.yellow("\u2298")} CLAUDE.md kept \u2014 modified since install. Remove manually if intended.`
4518
+ );
4519
+ }
4520
+ }
4521
+ const logWriteFailed = settleLog(
4522
+ { installLog, projectDir, selectedIds, keepTemplates, removedIds },
4523
+ { log, err, rm, writeLog }
4524
+ );
4525
+ for (const line of advisoryLines(plan, targetAssets, projectDir, keepTemplates, rootFiles)) {
4526
+ log(line);
4527
+ }
4528
+ const outcome = summarize({
4529
+ succeeded,
4530
+ failed,
4531
+ logWriteFailed,
4532
+ // 제거된 것도 없고 templates 도 안 지웠는데, 사용자에게 줄 방법조차 없을 때만 실패다.
4533
+ // ① `--only` = "이걸 빼라"는 특정 요청 — 하나도 못 뺐으면 요청 불이행 (C1).
4534
+ // ② 자동 경로가 없는 자산이 남았으면 — 안내할 명령조차 없다 (R1).
4535
+ // ③ 반면 global 자산만 남은 경우는 **정확한 수기 명령을 출력했으므로** 성공이다.
4536
+ // D16 설계대로의 정상이고, 여기까지 묶으면 global scope 사용자는 exit 0 을 영원히
4537
+ // 못 받는다 (로그가 지워져 재시도하면 더 나빠진다).
4538
+ nothingDone: succeeded === 0 && keepTemplates && (logSurvives || plan.noReversePath.length > 0)
4539
+ });
4540
+ log("");
4541
+ log(outcome.line);
4542
+ exit(outcome.code);
4543
+ }
4544
+ function summarize(r) {
4545
+ if (r.failed > 0)
4546
+ return {
4547
+ line: c.yellow(`uninstall finished with ${r.failed} skip(s) (${r.succeeded} ok)`),
4548
+ code: 1
4549
+ };
4550
+ if (r.nothingDone)
4551
+ return {
4552
+ line: c.yellow("\uC544\uBB34\uAC83\uB3C4 \uC790\uB3D9 \uC81C\uAC70\uB418\uC9C0 \uC54A\uC558\uB2E4 \u2014 \uC704 \uC548\uB0B4\uB97C \uB530\uB77C \uC9C1\uC811 \uCC98\uB9AC\uD574\uC57C \uD55C\uB2E4"),
4553
+ code: 1
4554
+ };
4555
+ if (r.logWriteFailed)
4556
+ return {
4557
+ line: c.yellow("\uC790\uC0B0\uC740 \uC81C\uAC70\uB410\uC73C\uB098 install log \uB97C \uAC31\uC2E0\uD558\uC9C0 \uBABB\uD588\uB2E4 (\uAE30\uB85D\uC774 \uC2E4\uC81C\uC640 \uB2E4\uB974\uB2E4)"),
4558
+ code: 1
4559
+ };
4560
+ return { line: status.success(c.green(`uninstall complete (${r.succeeded} asset(s))`)), code: 0 };
4561
+ }
4562
+ function headerLines(installLog, selectedIds, targetCount) {
4563
+ return [
4564
+ "",
4565
+ c.bold("uzys-agent-harness \xB7 uninstall"),
4566
+ "",
4567
+ c.dim(` installed: ${installLog.installedAt}`),
4568
+ c.dim(` scope: ${installLog.scope}`),
4569
+ c.dim(
4570
+ selectedIds ? ` assets: ${targetCount} selected of ${installLog.assets.length} (--only)` : ` assets: ${installLog.assets.length}`
4571
+ ),
4572
+ ""
4573
+ ];
4574
+ }
4575
+ function executeReverse(plan, log, logSurvives) {
4576
+ let succeeded = 0;
4577
+ let failed = 0;
4578
+ const removedIds = [];
4579
+ for (const step of plan.reverseSteps) {
4580
+ const result = step.execute();
4581
+ if (result.ok) {
4582
+ log(` ${status.success(step.label)}`);
4583
+ removedIds.push(step.assetId);
4584
+ succeeded++;
4585
+ } else {
4586
+ log(` ${c.yellow("\u2298")} ${step.label} (${result.message ?? "failed"})`);
4587
+ failed++;
4588
+ }
4589
+ }
4590
+ const tail = logSurvives ? "\uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C, \uAE30\uB85D \uC720\uC9C0" : "\uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C";
4591
+ for (const asset of plan.noReversePath) {
4592
+ log(` ${c.yellow("\u2298")} ${asset.id} (${asset.method}) \u2014 ${tail}`);
4593
+ }
4594
+ return { succeeded, failed, removedIds };
4595
+ }
4596
+ function settleLog(ctx, io) {
4597
+ const { installLog, projectDir, selectedIds, keepTemplates, removedIds } = ctx;
4598
+ if (selectedIds) {
4599
+ const remaining = installLog.assets.filter((a) => !removedIds.includes(a.id));
4600
+ try {
4601
+ io.writeLog(projectDir, { ...installLog, assets: remaining });
4602
+ io.log(` ${status.success(`install log updated (${remaining.length} asset(s) remain)`)}`);
4603
+ } catch (e2) {
4604
+ io.err(status.failure(c.red(`ERROR: install log \uAC31\uC2E0 \uC2E4\uD328 \u2014 ${installLogPath(projectDir)}`)));
4605
+ io.err(c.dim(` ${e2 instanceof Error ? e2.message : String(e2)}`));
4606
+ io.err(c.dim(` \uC2E4\uC81C\uB85C \uC81C\uAC70\uB41C \uC790\uC0B0: ${removedIds.join(", ") || "(\uC5C6\uC74C)"}`));
4607
+ return true;
4608
+ }
4609
+ } else if (keepTemplates) {
4610
+ io.rm(installLogPath(projectDir));
4611
+ io.log(` ${status.success("install log removed (templates kept)")}`);
4612
+ }
4613
+ return false;
4614
+ }
4615
+ function dryRunLines(plan, installLog, projectDir, keepTemplates, targetAssets, rootFiles) {
4616
+ const lines = [c.yellow("[DRY RUN] reverse list (\uC2E4\uC81C \uBCC0\uACBD \uC5C6\uC74C):"), ""];
4617
+ if (plan.reverseSteps.length === 0) {
4618
+ lines.push(c.dim(" (no project-scope assets to reverse)"));
4619
+ }
4620
+ lines.push(...plan.reverseSteps.map((s) => ` \u25CB ${s.label}`));
4621
+ lines.push(
4622
+ ...plan.noReversePath.map(
4623
+ (a) => c.dim(` \u2298 ${a.id} (${a.method}) \u2014 \uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C, \uAE30\uB85D \uC720\uC9C0`)
4624
+ )
4625
+ );
4626
+ if (!keepTemplates) {
4627
+ lines.push(` \u25CB remove templates: ${formatTemplateList(installLog)}`);
4628
+ if (installLog.templates.rootClaudeMd) {
4629
+ lines.push(
4630
+ rootClaudeMdModified(installLog, projectDir) ? " \u25CB keep CLAUDE.md (modified since install \u2014 preserved)" : " \u25CB remove CLAUDE.md"
4631
+ );
4632
+ }
4633
+ }
4634
+ lines.push(...advisoryLines(plan, targetAssets, projectDir, keepTemplates, rootFiles), "");
4635
+ return lines;
4636
+ }
4637
+ function advisoryLines(plan, targetAssets, projectDir, templatesKept, rootFiles) {
4638
+ const lines = [];
4639
+ if (plan.globalAdvisories.length > 0) {
4640
+ lines.push(
4641
+ "",
4642
+ c.yellow(
4643
+ `[GLOBAL] ${plan.globalAdvisories.length} asset(s) at scope=global \u2014 manual removal required (D16):`
4644
+ )
4645
+ );
4646
+ for (const adv of plan.globalAdvisories) {
4647
+ lines.push(c.dim(` \xB7 ${adv.asset.id} (${adv.asset.method})`), c.dim(` ${adv.command}`));
4648
+ }
4649
+ }
4650
+ if (templatesKept) lines.push(...manualAdvisoryLines(targetAssets, projectDir));
4651
+ lines.push(...rootFileAdvisoryLines(rootFiles, projectDir));
4652
+ return lines;
4653
+ }
4654
+ function rootFileAdvisoryLines(rootFiles, projectDir) {
4655
+ const present = rootFiles.filter((f) => existsSync14(join13(projectDir, f.path)));
4656
+ if (present.length === 0) return [];
4657
+ return [
4658
+ "",
4659
+ c.yellow("[ROOT] `.claude/` \uBC16\uC5D0 \uB0A8\uB294 \uAC83 (\uC790\uB3D9\uC73C\uB85C \uC9C0\uC6B0\uC9C0 \uC54A\uB294\uB2E4):"),
4660
+ ...present.flatMap((f) => [
4661
+ c.dim(
4662
+ ` \xB7 ${f.path} \u2014 ${f.change === "created" ? "\uD558\uB124\uC2A4\uAC00 \uC0DD\uC131 (\uC218\uC815\uD55C \uC801 \uC5C6\uC73C\uBA74 \uC0AD\uC81C\uD574\uB3C4 \uC548\uC804)" : "\uAE30\uC874 \uC0AC\uC6A9\uC790 \uD30C\uC77C\uC5D0 \uBCD1\uD569 (\uC9C1\uC811 \uD655\uC778 \uD544\uC694)"}`
4663
+ ),
4664
+ c.dim(` ${f.notes.join(" / ")}`)
4665
+ ])
4666
+ ];
4667
+ }
4668
+ function planReverse(assets, spawn) {
4669
+ const reverseSteps = [];
4670
+ const globalAdvisories = [];
4671
+ const noReversePath = [];
4672
+ for (const asset of assets) {
4673
+ if (asset.scope === "global") {
4674
+ globalAdvisories.push({ asset, command: buildGlobalAdvisoryCmd(asset) });
4675
+ continue;
4676
+ }
4677
+ const step = buildProjectReverseStep(asset, spawn);
4678
+ if (step) reverseSteps.push(step);
4679
+ else noReversePath.push(asset);
4680
+ }
4681
+ return { reverseSteps, globalAdvisories, noReversePath };
4682
+ }
4683
+ function buildProjectReverseStep(asset, spawn) {
4684
+ switch (asset.method) {
4685
+ case "plugin": {
4686
+ const pluginId = asset.detail.pluginId ?? asset.id;
4687
+ return {
4688
+ assetId: asset.id,
4689
+ label: `claude plugin uninstall --scope project ${pluginId}`,
4690
+ execute: () => {
4691
+ const r = spawn("claude", ["plugin", "uninstall", "--scope", "project", pluginId]);
4692
+ return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
4693
+ }
4694
+ };
4695
+ }
4696
+ case "skill": {
4697
+ const source = asset.detail.source ?? asset.id;
4698
+ return {
4699
+ assetId: asset.id,
4700
+ label: `npx skills remove ${source}`,
4701
+ execute: () => {
4702
+ const r = spawn("npx", [skillsCliSpec(), "remove", source, "--yes"]);
4703
+ return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
4704
+ }
4705
+ };
4706
+ }
4707
+ case "npm": {
4708
+ const pkg = asset.detail.pkg ?? asset.id;
4709
+ return {
4710
+ assetId: asset.id,
4711
+ label: `npm uninstall --save-dev ${pkg}`,
4712
+ execute: () => {
4713
+ const r = spawn("npm", ["uninstall", "--save-dev", pkg]);
4714
+ return r.status === 0 ? { ok: true } : { ok: false, message: (r.stderr || "").trim() };
4715
+ }
4716
+ };
4717
+ }
4718
+ case "npx-run":
4719
+ return null;
4720
+ case "shell-script":
4721
+ return null;
4722
+ case "internal":
4723
+ return null;
4724
+ }
4725
+ }
4726
+ function settingsHasHookCommand(projectDir, command) {
4727
+ const path = join13(projectDir, ".claude", "settings.json");
4728
+ if (!existsSync14(path)) return false;
4729
+ try {
4730
+ const parsed = JSON.parse(readFileSync13(path, "utf8"));
4731
+ return Object.values(parsed.hooks ?? {}).some(
4732
+ (matchers) => matchers.some((m) => m.hooks?.some((h2) => h2.command === command))
4733
+ );
4734
+ } catch {
4735
+ return false;
4736
+ }
4737
+ }
4738
+ function unknownIds(installLog, selectedIds) {
4739
+ const known = new Set(installLog.assets.map((a) => a.id));
4740
+ return selectedIds.filter((id) => !known.has(id));
4741
+ }
4742
+ function parseOnly(only) {
4743
+ if (!only) return null;
4744
+ const ids = only.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4745
+ return ids.length > 0 ? ids : null;
4746
+ }
4747
+ function manualAdvisoryLines(targetAssets, projectDir) {
4748
+ if (!targetAssets.some((a) => a.id === KARPATHY_ASSET_ID)) return [];
4749
+ const items = [];
4750
+ if (settingsHasHookCommand(projectDir, KARPATHY_HOOK_COMMAND))
4751
+ items.push(
4752
+ `.claude/settings.json \u2014 hooks.PreToolUse \uC5D0\uC11C \uB2E4\uC74C command \uD56D\uBAA9 \uC0AD\uC81C:
4753
+ ${KARPATHY_HOOK_COMMAND}`
4754
+ );
4755
+ if (existsSync14(join13(projectDir, KARPATHY_HOOK_RELPATH)))
4756
+ items.push(`\`${KARPATHY_HOOK_RELPATH}\` \u2014 \uC0AD\uC81C`);
4757
+ if (items.length === 0) return [];
4758
+ return [
4759
+ "",
4760
+ c.yellow("[MANUAL] \uC790\uB3D9\uC73C\uB85C \uB418\uB3CC\uB9AC\uC9C0 \uC54A\uC740 \uAC83 (\uC0AC\uC6A9\uC790 \uD3B8\uC9D1\uC774 \uC11E\uC774\uB294 \uD45C\uBA74):"),
4761
+ ...items.map((i) => c.dim(` \xB7 ${i}`))
4762
+ ];
4763
+ }
4764
+ function buildGlobalAdvisoryCmd(asset) {
4765
+ switch (asset.method) {
4766
+ case "plugin": {
4767
+ const pid = asset.detail.pluginId ?? asset.id;
4768
+ return `claude plugin uninstall --scope user ${pid}`;
4769
+ }
4770
+ case "skill": {
4771
+ const s = asset.detail.source ?? asset.id;
4772
+ return `npx skills remove -g ${s}`;
4773
+ }
4774
+ case "npm": {
4775
+ const pkg = asset.detail.pkg ?? asset.id;
4776
+ return `npm uninstall -g ${pkg}`;
4777
+ }
4778
+ case "npx-run":
4779
+ case "shell-script":
4780
+ case "internal":
4781
+ return "(no standard reverse \u2014 manual)";
4782
+ }
4783
+ }
4784
+ function removeTemplates(log, projectDir, rm) {
4785
+ rm(join13(projectDir, log.templates.claudeDir));
4786
+ if (log.templates.codexDir) rm(join13(projectDir, log.templates.codexDir));
4787
+ if (log.templates.opencodeDir) rm(join13(projectDir, log.templates.opencodeDir));
4788
+ const rootMd = log.templates.rootClaudeMd;
4789
+ if (rootMd) {
4790
+ if (rootClaudeMdModified(log, projectDir)) return { rootClaudeMdKept: true };
4791
+ rm(join13(projectDir, rootMd.path));
4792
+ }
4793
+ return { rootClaudeMdKept: false };
4794
+ }
4795
+ function rootClaudeMdModified(log, projectDir) {
4796
+ const rootMd = log.templates.rootClaudeMd;
4797
+ if (!rootMd) return false;
4798
+ const path = join13(projectDir, rootMd.path);
4799
+ if (!existsSync14(path)) return false;
4800
+ return hashContent(readFileSync13(path, "utf8")) !== rootMd.sha256;
4801
+ }
4802
+ function formatTemplateList(log) {
4803
+ const items = [log.templates.claudeDir];
4804
+ if (log.templates.codexDir) items.push(log.templates.codexDir);
4805
+ if (log.templates.opencodeDir) items.push(log.templates.opencodeDir);
4806
+ return items.join(", ");
4807
+ }
4808
+ function defaultSpawn2(cmd, args) {
4809
+ return spawnSync2(cmd, [...args], { encoding: "utf8", stdio: "pipe", timeout: 12e4 });
4810
+ }
4811
+ function defaultRm(path) {
4812
+ if (existsSync14(path)) {
4813
+ rmSync(path, { recursive: true, force: true });
4814
+ }
4815
+ }
4816
+ function registerUninstallCommand(cli2) {
4817
+ cli2.command("uninstall", "Uninstall harness assets (log-based reverse)").option("--project-dir <path>", "[Project] Target project directory", {
4818
+ default: process.cwd()
4819
+ }).option("--dry-run", "[Mode] List reverse steps without executing").option(
4820
+ "--keep-templates",
4821
+ "[Mode] Keep `.claude/`, `.codex/`, `.opencode/` templates (remove only external assets)"
4822
+ ).option(
4823
+ "--only <ids>",
4824
+ "[Scope] Remove only these assets (comma-separated ids from `agent-harness list`). Templates untouched"
4825
+ ).option("--yes", "[Mode] Skip the interactive picker and remove everything (non-interactive)").action(async (options) => {
4826
+ await dispatchUninstall(options);
4827
+ });
4828
+ }
4829
+ function shouldRunInteractive(options, isTty) {
4830
+ if (!isTty) return false;
4831
+ if (options.yes || options.dryRun) return false;
4832
+ return options.only === void 0;
4833
+ }
4834
+ async function dispatchUninstall(options) {
4835
+ if (!shouldRunInteractive(options, Boolean(process.stdin.isTTY))) {
4836
+ uninstallAction(options);
4837
+ return;
4838
+ }
4839
+ const projectDir = resolve4(options.projectDir ?? process.cwd());
4840
+ const picked = await runInteractiveUninstall(projectDir);
4841
+ if (!picked.ok || !picked.options) {
4842
+ if (picked.reason === "no-log") {
4843
+ console.error(
4844
+ status.failure(c.red(`ERROR: install log not found at ${installLogPath(projectDir)}`))
4845
+ );
4846
+ console.error(c.dim(" Nothing installed here by agent-harness."));
4847
+ process.exit(1);
4848
+ }
4849
+ return;
4850
+ }
4851
+ uninstallAction(picked.options);
4852
+ }
4853
+
4854
+ // src/interactive.ts
4855
+ init_esm_shims();
4856
+
4857
+ // src/prompts.ts
4858
+ init_esm_shims();
4859
+
4366
4860
  // src/router.ts
4367
4861
  init_esm_shims();
4368
4862
  function buildRouterChoices(state) {
@@ -4466,7 +4960,8 @@ function expandDevMethodBundle(ids) {
4466
4960
  const rest = ids.filter((v2) => v2 !== DEV_METHOD_BUNDLE_VALUE);
4467
4961
  return ids.includes(DEV_METHOD_BUNDLE_VALUE) ? [...rest, ...bundleMemberValues()] : rest;
4468
4962
  }
4469
- function buildPageGroups(cats, initialSet) {
4963
+ function buildPageGroups(cats, initialSet, installedSet = /* @__PURE__ */ new Set()) {
4964
+ const installedMark = (value) => installedSet.has(value) ? " \u25CF installed" : "";
4470
4965
  const groups = {};
4471
4966
  const flatItems = [];
4472
4967
  for (const cat of cats) {
@@ -4475,14 +4970,14 @@ function buildPageGroups(cats, initialSet) {
4475
4970
  items.push({
4476
4971
  value: `option:${o.key}`,
4477
4972
  // v26.62.3 — group header 와 옵션 사이 시각 hierarchy 강화. label prefix 4 space.
4478
- label: ` ${o.label} [${o.source}]`,
4973
+ label: ` ${o.label} [${o.source}]${installedMark(`option:${o.key}`)}`,
4479
4974
  hint: o.hint
4480
4975
  });
4481
4976
  }
4482
4977
  if (cat === DEV_METHOD_BUNDLE_CATEGORY) {
4483
4978
  items.push({
4484
4979
  value: DEV_METHOD_BUNDLE_VALUE,
4485
- label: ` uzys \uD558\uB124\uC2A4 \uBC29\uBC95\uB860 ${DEV_METHOD_SKILL_IDS.length}\uC885 [uzys] \u2605 official`,
4980
+ label: ` uzys \uD558\uB124\uC2A4 \uBC29\uBC95\uB860 ${DEV_METHOD_SKILL_IDS.length}\uC885 [uzys] \u2605 official${installedMark(DEV_METHOD_BUNDLE_VALUE)}`,
4486
4981
  hint: DEV_METHOD_SKILL_IDS.join(", ")
4487
4982
  });
4488
4983
  }
@@ -4494,7 +4989,7 @@ function buildPageGroups(cats, initialSet) {
4494
4989
  const badge = tier === "official" ? " \u2605 official" : tier === "experimental" ? " \u26A0 experimental (opt-in)" : "";
4495
4990
  items.push({
4496
4991
  value: `asset:${a.id}`,
4497
- label: ` ${a.id} [${a.source}]${badge}`,
4992
+ label: ` ${a.id} [${a.source}]${badge}${installedMark(`asset:${a.id}`)}`,
4498
4993
  hint: a.description
4499
4994
  });
4500
4995
  }
@@ -4609,6 +5104,9 @@ Proceed?`,
4609
5104
  const initialSet = new Set(displayInitial);
4610
5105
  const collected = new Set(displayInitial);
4611
5106
  const recapLine = recap ? `Tracks: ${recap.tracks.join(", ")} \xB7 CLIs: ${recap.cli.join(", ")}` : "";
5107
+ const installedSet = new Set(
5108
+ collapseDevMethodBundle(recap?.installed ?? [])
5109
+ );
4612
5110
  process.stdout.write("\x1B[?1049h");
4613
5111
  let resultIds = null;
4614
5112
  let aborted = false;
@@ -4617,7 +5115,7 @@ Proceed?`,
4617
5115
  while (pageIdx < pages.length) {
4618
5116
  const page = pages[pageIdx];
4619
5117
  if (!page) break;
4620
- const { groups, flatItems } = buildPageGroups(page.cats, initialSet);
5118
+ const { groups, flatItems } = buildPageGroups(page.cats, initialSet, installedSet);
4621
5119
  const selectedNow = flatItems.filter((it3) => collected.has(it3.value)).map((it3) => it3.value);
4622
5120
  const pageDefault = flatItems.filter((it3) => initialSet.has(it3.value)).length;
4623
5121
  const totalSelected = collected.size;
@@ -4625,6 +5123,9 @@ Proceed?`,
4625
5123
  `Step ${step.current}/${step.total} \xB7 Page ${pageIdx + 1}/${pages.length} \xB7 ${page.label}`,
4626
5124
  recapLine ? ` ${recapLine}` : "",
4627
5125
  ` Selected so far: ${totalSelected} items \xB7 This page default \u2713 ${pageDefault}/${flatItems.length}`,
5126
+ // 마커의 뜻을 화면에서 바로 알려준다 — 체크 해제를 제거로 오해하는 것이 이 화면의
5127
+ // 원래 문제였으므로, 마커가 보일 때는 제거 경로를 같은 자리에서 말한다.
5128
+ installedSet.size > 0 ? " \u25CF installed = \uC774\uBBF8 \uC124\uCE58\uB428 \xB7 \uCCB4\uD06C \uD574\uC81C\uD574\uB3C4 \uC81C\uAC70\uB418\uC9C0 \uC54A\uB294\uB2E4 (\uC81C\uAC70: agent-harness uninstall)" : "",
4628
5129
  " Space toggle \xB7 Enter \u2192 next \xB7 ESC \u2192 prev"
4629
5130
  ].filter(Boolean).join("\n");
4630
5131
  const groupOpts = {
@@ -4666,8 +5167,8 @@ Proceed?`,
4666
5167
 
4667
5168
  // src/state.ts
4668
5169
  init_esm_shims();
4669
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
4670
- import { join as join13 } from "path";
5170
+ import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
5171
+ import { join as join14 } from "path";
4671
5172
  var META_FILE = ".claude/.installed-tracks";
4672
5173
  var LEGACY_SIGNATURES = [
4673
5174
  { rule: "htmx.md", track: "ssr-htmx" },
@@ -4677,13 +5178,13 @@ var LEGACY_SIGNATURES = [
4677
5178
  { rule: "cli-development.md", track: "tooling" }
4678
5179
  ];
4679
5180
  function detectInstallState(projectDir) {
4680
- const claudeDir = join13(projectDir, ".claude");
4681
- const hasClaudeDir = existsSync14(claudeDir);
5181
+ const claudeDir = join14(projectDir, ".claude");
5182
+ const hasClaudeDir = existsSync15(claudeDir);
4682
5183
  if (!hasClaudeDir) {
4683
5184
  return { state: "new", tracks: [], source: "none", hasClaudeDir: false };
4684
5185
  }
4685
- const metaPath = join13(projectDir, META_FILE);
4686
- if (existsSync14(metaPath)) {
5186
+ const metaPath = join14(projectDir, META_FILE);
5187
+ if (existsSync15(metaPath)) {
4687
5188
  const tracks2 = readMetafile(metaPath);
4688
5189
  return { state: "existing", tracks: tracks2, source: "metafile", hasClaudeDir: true };
4689
5190
  }
@@ -4691,7 +5192,7 @@ function detectInstallState(projectDir) {
4691
5192
  return { state: "existing", tracks, source: "legacy", hasClaudeDir: true };
4692
5193
  }
4693
5194
  function readMetafile(path) {
4694
- const raw = readFileSync13(path, "utf8");
5195
+ const raw = readFileSync14(path, "utf8");
4695
5196
  const seen = /* @__PURE__ */ new Set();
4696
5197
  for (const line of raw.split(/\s+/)) {
4697
5198
  const trimmed = line.trim();
@@ -4702,13 +5203,13 @@ function readMetafile(path) {
4702
5203
  return [...seen].sort();
4703
5204
  }
4704
5205
  function inferFromLegacySignatures(projectDir) {
4705
- const rulesDir = join13(projectDir, ".claude/rules");
4706
- if (!existsSync14(rulesDir)) {
5206
+ const rulesDir = join14(projectDir, ".claude/rules");
5207
+ if (!existsSync15(rulesDir)) {
4707
5208
  return [];
4708
5209
  }
4709
5210
  const found = /* @__PURE__ */ new Set();
4710
5211
  for (const sig of LEGACY_SIGNATURES) {
4711
- if (existsSync14(join13(rulesDir, sig.rule))) {
5212
+ if (existsSync15(join14(rulesDir, sig.rule))) {
4712
5213
  found.add(sig.track);
4713
5214
  }
4714
5215
  }
@@ -4736,6 +5237,19 @@ function toOptionFlags(keys) {
4736
5237
  withKarpathyHook: picked.has("withKarpathyHook")
4737
5238
  };
4738
5239
  }
5240
+ function installedTargetState(projectDir) {
5241
+ const log = readInstallLog(projectDir);
5242
+ if (!log) return { installed: [], projectScoped: [] };
5243
+ return {
5244
+ installed: log.assets.map((a) => a.id),
5245
+ projectScoped: log.assets.filter((a) => a.scope !== "global").map((a) => a.id)
5246
+ };
5247
+ }
5248
+ function initialTargetSelection(tracks, installedProjectAssetIds) {
5249
+ const ids = new Set(recommendedExternalAssets(tracks));
5250
+ for (const id of installedProjectAssetIds) ids.add(id);
5251
+ return [...ids].map((id) => `asset:${id}`);
5252
+ }
4739
5253
  async function runInteractive(projectDir, deps = {}) {
4740
5254
  const prompts = deps.prompts ?? defaultPrompts;
4741
5255
  const detect = deps.detect ?? detectInstallState;
@@ -4749,6 +5263,7 @@ async function runInteractive(projectDir, deps = {}) {
4749
5263
  }
4750
5264
  prompts.intro("uzys-agent-harness installer");
4751
5265
  const state = detect(projectDir);
5266
+ const installed = (deps.readInstalled ?? installedTargetState)(projectDir);
4752
5267
  let initialTracks;
4753
5268
  let mode = "fresh";
4754
5269
  if (state.state === "existing") {
@@ -4824,10 +5339,11 @@ ${summary}`);
4824
5339
  cli2 = result;
4825
5340
  step = "targets";
4826
5341
  } else if (step === "targets") {
4827
- const initial = targetSelections !== null ? [...targetSelections] : recommendedExternalAssets(tracks ?? []).map((id) => `asset:${id}`);
5342
+ const initial = targetSelections !== null ? [...targetSelections] : initialTargetSelection(tracks ?? [], installed.projectScoped);
4828
5343
  const result = await prompts.selectInstallTargets(initial, WIZARD.TARGETS, {
4829
5344
  tracks: tracks ?? [],
4830
- cli: cli2 ?? ["claude"]
5345
+ cli: cli2 ?? ["claude"],
5346
+ installed: installed.installed.map((id) => `asset:${id}`)
4831
5347
  });
4832
5348
  if (result === null) {
4833
5349
  step = "cli";
@@ -4973,6 +5489,7 @@ function buildCli() {
4973
5489
  cli2.help();
4974
5490
  cli2.version(VERSION);
4975
5491
  registerInstallCommand(cli2);
5492
+ registerListCommand(cli2);
4976
5493
  registerUninstallCommand(cli2);
4977
5494
  cli2.command("", "Interactive installer (state detection + prompts)").action(() => defaultAction());
4978
5495
  return cli2;