@uzysjung/agent-harness 26.161.0 → 26.163.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
@@ -16,21 +16,20 @@ AI 코딩 도구에 꼭 필요한 룰·훅·스킬만 남기고 나머지는 모
16
16
 
17
17
  ## 빠른 시작
18
18
 
19
- Node 20 이상이 필요하다. 프로젝트 폴더에서 다음 명령을 실행한다:
19
+ Node 20.12 이상이 필요하다. 프로젝트 폴더에서 다음 명령을 실행한다:
20
20
 
21
21
  ```bash
22
22
  npx -y @uzysjung/agent-harness
23
23
  ```
24
24
 
25
- 위저드가 다음 여섯 단계를 안내한다:
25
+ 위저드가 다음 다섯 단계를 안내한다:
26
26
 
27
27
  ```
28
- 1/6 Tracks 무엇을 만드는지(스택) — 항목을 미리 체크해 줄 뿐이다
29
- 2/6 CLI claude / codex / opencode / antigravity — 여러 개 가능
30
- 3/6 Install items 트랙에 맞는 항목이 미리 체크되어 있다. 원치 않는 것은 해제
31
- 4/6 Scope Project(기본값, 이 폴더에만) 또는 Global
32
- 5/6 Confirm 요약과 함께, 이 선택이 매 세션에 얹는 컨텍스트 크기를 보여 준다
33
- 6/6 Installing
28
+ 1/5 Tracks 무엇을 만드는지(스택) — 항목을 미리 체크해 줄 뿐이다
29
+ 2/5 CLI claude / codex / opencode / antigravity — 여러 개 가능
30
+ 3/5 Install items 트랙에 맞는 항목이 미리 체크되어 있다. 원치 않는 것은 해제
31
+ 4/5 Confirm 요약과 함께, 이 선택이 매 세션에 얹는 컨텍스트 크기를 보여 준다
32
+ 5/5 Installing
34
33
  ```
35
34
 
36
35
  그다음 같은 폴더에서 AI 코딩 도구를 실행하면, 첫 세션부터 룰과 스킬이 바로 적용된다:
@@ -51,7 +50,7 @@ claude # 또는 codex / opencode / agy
51
50
  | **훅** | CLI 가 스스로 실행하는 스크립트. Claude Code 에는 2개가 들어간다: 하나는 세션을 시작할 때 스펙과 변경 기록을 불러오고, 다른 하나는 `.env` · lock 파일 · 인증서 편집을 막는다 — 하네스에서 "안 된다"고 제한하는 유일한 장치이며, 편집을 막을 때마다 로그를 한 줄 남긴다 | 자동으로 — 세션을 시작할 때, 편집하기 직전에 |
52
51
  | **스킬** | 작업에 필요할 때 에이전트가 열어보는 단계별 절차서. 이 저장소에서 관리하는 방법론 스킬과 트랙에 필요한 기술 스택 스킬이 들어 있다(예: `csr-supabase` 의 React · shadcn · Supabase · Postgres) | 필요할 때만 — 한 줄짜리 설명만 항상 띄워 두고, 본문은 사용할 때만 읽는다 |
53
52
  | **에이전트** | 메인 에이전트가 작업을 맡기는 조수. 모든 트랙에 독립 검증자 `reviewer` 가 있고, 개발 트랙에는 `implementer` 가 있으며, 그것을 쓰는 트랙에만 `data-analyst` · `strategist` 가 들어간다 | 메인 에이전트가 작업을 위임할 때 |
54
- | **앵커** | CLI 가 매 세션마다 읽는 작업 원칙 파일. 원래 있던 내 `CLAUDE.md` 파일은 그대로 유지된다 — 하네스는 import 구문 한 줄만 추가할 뿐 나머지는 건드리지 않는다([어느 파일이 누구 것인가](docs/CONTEXT-FILES.md)) | 매 세션 |
53
+ | **앵커** | CLI 가 매 세션마다 읽는 작업 원칙 파일. 원래 있던 내 `CLAUDE.md` 파일은 그대로 유지된다 — 하네스는 표식으로 감싼 import 블록(`@CLAUDE-uzys-harness.md` 참조와 그 아래 스킬 안내)만 추가할 뿐 나머지는 건드리지 않는다([어느 파일이 누구 것인가](docs/CONTEXT-FILES.md)) | 매 세션 |
55
54
 
56
55
  모든 트랙에 공통으로 들어가는 방법론 스킬은 4가지다: `north-star` · `objective-brief` · `gh-issue-workflow` · `audit-harness-fit`. 번들 스킬은 `--with` 나 `--without` 뒤에 이름을 적어 추가하거나 뺄 수 있다.
57
56
 
@@ -94,7 +93,7 @@ claude # 또는 codex / opencode / agy
94
93
 
95
94
  **기존 프로젝트에 적용해도 안전하다.** 내가 직접 수정한 파일을 교체해야 할 때는 타임스탬프를 붙여 백업본을 남기고 그 경로를 알려 준다. 내가 작성하거나 수정한 파일은 백업 없이 지우지 않으며, 기존에 있던 `.mcp.json` 서버 설정은 덮어쓰지 않고 내용을 병합한다([기존 프로젝트에 설치하기](docs/USAGE.md#installing-into-an-existing-project)).
96
95
 
97
- **기본적으로 현재 프로젝트에만 설치된다.** 위저드 4단계에서 Global 을 선택하지 않는 한 `~/.codex/` · `~/.opencode/` · `~/.gemini/` 디렉터리나 전역 npm 환경에는 아무것도 설치하지 않는다. 유일한 예외는 Claude Code 플러그인이다 — `claude` CLI 는 범위를 불문하고 플러그인 캐시를 `~/.claude/plugins/` 디렉터리에 저장하며 프로젝트는 메타데이터로 구분하기 때문이다. `.claude/` 디렉터리 밖에는 `.mcp.json`, `.gitignore` 파일에 추가하는 몇 줄(파일이 이미 있을 때), Supabase 트랙의 경우 `.env.example`, 그리고 설치 기록을 남기는 `.uzys-agent-harness/` 디렉터리만 생성한다 — [전체 설치 목록 보기](docs/USAGE.md#what-the-harness-writes).
96
+ **현재 프로젝트에만 설치된다.** `~/.opencode/` · `~/.gemini/` 디렉터리나 전역 npm 환경에는 아무것도 설치하지 않는다. 프로젝트 밖에 쓰는 옵트인 예외는 둘뿐이다 — Claude Code 플러그인(`claude` CLI 가 플러그인 캐시를 `~/.claude/plugins/` 에 저장하며 프로젝트는 메타데이터로 구분), 그리고 `--with-codex-trust`(Codex 가 프로젝트 설정을 읽도록 `~/.codex/config.toml` 에 `[projects]` trust 항목 하나 추가 — [자세히](docs/USAGE.md#scope)). `.claude/` 디렉터리 밖에는 `CLAUDE.md`/`AGENTS.md` 스캐폴드와 하네스 앵커, `.mcp.json`, `.gitignore` 파일에 추가하는 몇 줄(파일이 이미 있을 때), `csr-supabase`(및 `full`) 트랙의 `.env.example`, `--with ci-scaffold` 를 줬을 때만 `.github/workflows/`, 그리고 설치 기록을 남기는 `.uzys-agent-harness/` 디렉터리를 생성한다 — [전체 설치 목록 보기](docs/USAGE.md#what-the-harness-writes).
98
97
 
99
98
  ## 다른 도구를 이미 쓰고 있다면
100
99
 
package/README.md CHANGED
@@ -16,21 +16,20 @@ Works with **Claude Code** · **Codex** · **OpenCode** · **Antigravity**.
16
16
 
17
17
  ## Quick start
18
18
 
19
- You need Node 20 or newer. Run this in your project folder:
19
+ You need Node 20.12 or newer. Run this in your project folder:
20
20
 
21
21
  ```bash
22
22
  npx -y @uzysjung/agent-harness
23
23
  ```
24
24
 
25
- The wizard asks six things:
25
+ The wizard asks five things:
26
26
 
27
27
  ```
28
- 1/6 Tracks what you are building (a stack) — this only pre-checks items
29
- 2/6 CLI claude / codex / opencode / antigravity — one or more
30
- 3/6 Install items everything is pre-checked for your track; uncheck what you don't want
31
- 4/6 Scope Project (default, this folder only) or Global
32
- 5/6 Confirm summary, plus how much context your selection adds to each session
33
- 6/6 Installing
28
+ 1/5 Tracks what you are building (a stack) — this only pre-checks items
29
+ 2/5 CLI claude / codex / opencode / antigravity — one or more
30
+ 3/5 Install items everything is pre-checked for your track; uncheck what you don't want
31
+ 4/5 Confirm summary, plus how much context your selection adds to each session
32
+ 5/5 Installing
34
33
  ```
35
34
 
36
35
  Then open your AI coding tool in the same folder. The rules and skills are live from the first session:
@@ -51,7 +50,7 @@ No terminal for the wizard (CI, containers, scripts)? Use flags: `install --trac
51
50
  | **Hooks** | Scripts your CLI runs on its own. Two, on Claude Code: one loads your spec and change log at session start; one blocks edits to `.env`, lock files, and certificates — the only thing in the harness that says "no", and it logs one line each time | Automatically, at session start or before an edit |
52
51
  | **Skills** | Step-by-step playbooks the agent opens when a task calls for them — the method skills written in this repo, plus the stack skills your track needs (for example React, shadcn, Supabase, Postgres on `csr-supabase`) | Only when relevant — a one-line description stays loaded, the body loads on use |
53
52
  | **Agents** | Helpers the main agent can hand work to: an independent `reviewer` on every track, `implementer` on dev tracks, `data-analyst` and `strategist` on the tracks that use them | When the main agent delegates |
54
- | **Anchor** | One working-principles file your CLI reads every session. Your own `CLAUDE.md` stays yours — the harness adds one import line and never touches the rest ([which file is whose](docs/CONTEXT-FILES.md)) | Every session |
53
+ | **Anchor** | One working-principles file your CLI reads every session. Your own `CLAUDE.md` stays yours — the harness adds one marked import block (the `@CLAUDE-uzys-harness.md` reference and the skills guidance that follows it) and never touches the rest ([which file is whose](docs/CONTEXT-FILES.md)) | Every session |
55
54
 
56
55
  Four method skills go to every track: `north-star`, `objective-brief`, `gh-issue-workflow`, `audit-harness-fit`. Bundled skills can be added or dropped by name with `--with` / `--without`.
57
56
 
@@ -94,7 +93,7 @@ In a terminal, `uninstall` offers three choices — one CLI, selected assets, or
94
93
 
95
94
  **Safe on an existing project.** Before replacing a file you edited, the harness writes a timestamped backup next to it and prints the path. Nothing you wrote or edited is deleted without a backup beside it, and your existing `.mcp.json` servers are merged, not replaced ([installing into an existing project](docs/USAGE.md#installing-into-an-existing-project)).
96
95
 
97
- **Your project only, by default.** Nothing goes to `~/.codex/`, `~/.opencode/`, `~/.gemini/`, or global npm unless you choose Global at step 4. Claude Code plugins are the one exception: the `claude` CLI keeps its plugin cache under `~/.claude/plugins/` in either scope and isolates projects by metadata. Besides `.claude/`, install writes `.mcp.json`, a few `.gitignore` lines (when that file exists), an `.env.example` on Supabase tracks, and its own record at `.uzys-agent-harness/` — [the full list](docs/USAGE.md#what-the-harness-writes).
96
+ **Your project only.** Nothing goes to `~/.opencode/`, `~/.gemini/`, or global npm. Two opt-in exceptions write outside the project: Claude Code plugins (the `claude` CLI keeps its plugin cache under `~/.claude/plugins/` and isolates projects by metadata), and `--with-codex-trust`, which adds one `[projects]` trust entry to `~/.codex/config.toml` so Codex reads its project config ([details](docs/USAGE.md#scope)). Besides `.claude/`, install writes `CLAUDE.md`/`AGENTS.md` scaffolds and their harness anchor, `.mcp.json`, a few `.gitignore` lines (when that file exists), an `.env.example` on `csr-supabase` (and `full`), `.github/workflows/` only with `--with ci-scaffold`, and its own record at `.uzys-agent-harness/` — [the full list](docs/USAGE.md#what-the-harness-writes).
98
97
 
99
98
  ## Already using another tool?
100
99
 
@@ -156,6 +156,22 @@ var EXTERNAL_ASSETS = [
156
156
  // === Repo-bundled internal skills (uzys 1st-party, v26.87.0) ===
157
157
  // 2026-08-02 복원 (ADR-062) — 이관(ADR-060) 후 감사에서 본문 열화 104건(dropped 76 ·
158
158
  // damaged 28)이 확인돼 9종을 이 리포 번들로 되돌렸다. 아래 10종 전부 `kind: "internal"`.
159
+ // #602 — ui-visual-review: TRACKS.md 가 말하는 트랙 추종 번들 스킬("follows the track, not
160
+ // the catalog"). internal 카탈로그 자산으로 등록해 selectedInternalSkills 흐름에 태우면
161
+ // .agents/skills(비-Claude 자리)에도 깔린다 — 종전의 .claude/ 전용 배선과 어긋나던 것.
162
+ {
163
+ id: "ui-visual-review",
164
+ tier: "official",
165
+ // uzys 자사 스킬
166
+ description: "UI visual review \u2014 screenshot capture, baseline diff, and a review gate for UI changes (ships with UI tracks; ssr-htmx excluded)",
167
+ category: "frontend",
168
+ source: "uzys",
169
+ condition: {
170
+ kind: "any-track",
171
+ tracks: ["csr-supabase", "csr-fastify", "csr-fastapi", "ssr-nextjs", "full"]
172
+ },
173
+ method: { kind: "internal", key: "ui-visual-review" }
174
+ },
159
175
  {
160
176
  id: "compaction-handoff",
161
177
  tier: "official",
@@ -870,6 +886,10 @@ var DEV_METHOD_SKILL_IDS = [
870
886
  ];
871
887
  var INTERNAL_BUNDLED_SKILL_IDS = [
872
888
  ...DEV_METHOD_SKILL_IDS,
889
+ // #602 — 트랙을 따라가는 번들 스킬(csr-*·ssr-*·full). selectedInternalSkills 로 흘러야
890
+ // .agents/skills(비-Claude 자리)에도 깔린다 — manifest 의 UI_SKILL_DIRS 배선은 .claude/ 만
891
+ // 알아 TRACKS.md 와 어긋났다. 조건은 buildAssetSpec 이 UI 트랙으로 건다.
892
+ "ui-visual-review",
873
893
  "north-star",
874
894
  "gh-issue-workflow",
875
895
  "objective-brief",
@@ -1134,13 +1154,31 @@ ${block}
1134
1154
  `;
1135
1155
  }
1136
1156
  function stripHarnessImport(text) {
1137
- const start = text.indexOf(IMPORT_MARKER_START);
1138
- if (start === -1) return null;
1139
- const endAt = text.indexOf(IMPORT_MARKER_END, start);
1140
- if (endAt === -1) return null;
1141
- const before = text.slice(0, start).replace(/\n+$/, "\n");
1142
- const after = text.slice(endAt + IMPORT_MARKER_END.length).replace(/^\n+/, "");
1143
- return `${before}${after}`;
1157
+ if (!text.includes(IMPORT_MARKER_START) && !text.includes(IMPORT_MARKER_END)) return null;
1158
+ const endsWithNewline = text.endsWith("\n");
1159
+ const out = [];
1160
+ const lines = text.split("\n");
1161
+ let i = 0;
1162
+ while (i < lines.length) {
1163
+ const line = lines[i] ?? "";
1164
+ if (line.includes(IMPORT_MARKER_START)) {
1165
+ let j = i;
1166
+ while (j < lines.length && !(lines[j] ?? "").includes(IMPORT_MARKER_END)) j++;
1167
+ i = j < lines.length ? j + 1 : i + 1;
1168
+ if (out.length > 0 && out[out.length - 1] === "") out.pop();
1169
+ continue;
1170
+ }
1171
+ if (line.includes(IMPORT_MARKER_END) || line.trim() === HARNESS_IMPORT_LINE) {
1172
+ i++;
1173
+ continue;
1174
+ }
1175
+ out.push(line);
1176
+ i++;
1177
+ }
1178
+ let result = out.join("\n");
1179
+ if (endsWithNewline && result !== "" && !result.endsWith("\n")) result += "\n";
1180
+ if (!endsWithNewline && result.endsWith("\n")) result = result.slice(0, -1);
1181
+ return result === text ? null : result;
1144
1182
  }
1145
1183
  function hasHarnessImport(text) {
1146
1184
  if (text.includes(IMPORT_MARKER_START)) return true;
@@ -1359,8 +1397,8 @@ init_esm_shims();
1359
1397
 
1360
1398
  // src/install-log.ts
1361
1399
  init_esm_shims();
1362
- import { createHash } from "crypto";
1363
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync } from "fs";
1400
+ import { createHash as createHash2 } from "crypto";
1401
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync } from "fs";
1364
1402
  import { dirname as dirname2, join as join3 } from "path";
1365
1403
 
1366
1404
  // src/cli-targets.ts
@@ -1414,17 +1452,20 @@ function targetsInclude(targets, base) {
1414
1452
 
1415
1453
  // src/fs-ops.ts
1416
1454
  init_esm_shims();
1455
+ import { createHash } from "crypto";
1417
1456
  import {
1418
1457
  copyFileSync,
1419
- cpSync,
1420
1458
  existsSync as existsSync2,
1421
1459
  constants as fsConstants,
1422
1460
  mkdirSync,
1423
1461
  readdirSync,
1424
1462
  readFileSync as readFileSync2,
1463
+ readlinkSync,
1425
1464
  realpathSync,
1426
1465
  renameSync,
1427
- rmdirSync
1466
+ rmdirSync,
1467
+ rmSync,
1468
+ symlinkSync
1428
1469
  } from "fs";
1429
1470
  import { dirname, join as join2 } from "path";
1430
1471
  function ensureDir(path2) {
@@ -1480,17 +1521,27 @@ function copyBackupDir(target, now = /* @__PURE__ */ new Date()) {
1480
1521
  return null;
1481
1522
  }
1482
1523
  const backup = claimBackupPath(`${target}.backup-${formatStamp(now)}`, (c) => mkdirSync(c));
1483
- cpSync(realpathSync(target), backup, { recursive: true });
1524
+ try {
1525
+ copyTreePreservingLinks(realpathSync(target), backup);
1526
+ } catch (e) {
1527
+ rmSync(backup, { recursive: true, force: true });
1528
+ const detail = e instanceof Error ? e.message : String(e);
1529
+ throw new Error(`backup of ${target} failed, partial copy removed \u2014 ${detail}`);
1530
+ }
1484
1531
  return backup;
1485
1532
  }
1486
- function backupFileIfChanged(target, newContent, now = /* @__PURE__ */ new Date()) {
1487
- if (!existsSync2(target)) {
1488
- return null;
1489
- }
1490
- if (readFileSync2(target, "utf-8") === newContent) {
1491
- return null;
1533
+ function copyTreePreservingLinks(source, target) {
1534
+ mkdirSync(target, { recursive: true });
1535
+ for (const entry of readdirSync(source, { withFileTypes: true })) {
1536
+ const src = join2(source, entry.name);
1537
+ const dst = join2(target, entry.name);
1538
+ if (entry.isSymbolicLink()) {
1539
+ symlinkSync(readlinkSync(src), dst);
1540
+ continue;
1541
+ }
1542
+ if (entry.isDirectory()) copyTreePreservingLinks(src, dst);
1543
+ else if (entry.isFile()) copyFile(src, dst);
1492
1544
  }
1493
- return backupFile(target, now);
1494
1545
  }
1495
1546
  function backupFile(target, now = /* @__PURE__ */ new Date()) {
1496
1547
  return claimBackupPath(
@@ -1527,6 +1578,45 @@ function ensureProjectSkeleton(projectDir) {
1527
1578
  mkdirSync(join2(projectDir, d), { recursive: true });
1528
1579
  }
1529
1580
  }
1581
+ function dirTreesIdentical(a, b) {
1582
+ if (!existsSync2(a) || !existsSync2(b)) return false;
1583
+ const signature = (root, rel = "", out = /* @__PURE__ */ new Map()) => {
1584
+ for (const e of readdirSync(join2(root, rel), { withFileTypes: true })) {
1585
+ const r = rel === "" ? e.name : `${rel}/${e.name}`;
1586
+ if (e.isSymbolicLink()) out.set(r, `link:${readlinkSync(join2(root, r))}`);
1587
+ else if (e.isDirectory()) signature(root, r, out);
1588
+ else if (e.isFile()) {
1589
+ out.set(
1590
+ r,
1591
+ createHash("sha256").update(readFileSync2(join2(root, r))).digest("hex")
1592
+ );
1593
+ }
1594
+ }
1595
+ return out;
1596
+ };
1597
+ const sa = signature(a);
1598
+ const sb = signature(b);
1599
+ if (sa.size !== sb.size) return false;
1600
+ for (const [rel, sha] of sa) {
1601
+ if (sb.get(rel) !== sha) return false;
1602
+ }
1603
+ return true;
1604
+ }
1605
+ function backupIfLossyUtf8(target, now = /* @__PURE__ */ new Date()) {
1606
+ if (!existsSync2(target)) return null;
1607
+ let raw;
1608
+ try {
1609
+ raw = readFileSync2(target);
1610
+ } catch {
1611
+ return null;
1612
+ }
1613
+ if (Buffer.from(raw.toString("utf8")).equals(raw)) return null;
1614
+ const backup = backupFile(target, now);
1615
+ console.error(
1616
+ `\u26A0 ${target} contains bytes that are not valid UTF-8 \u2014 rewriting would alter them permanently. Original bytes saved as ${backup}`
1617
+ );
1618
+ return backup;
1619
+ }
1530
1620
 
1531
1621
  // src/install-log.ts
1532
1622
  var INSTALL_LOG_FILENAME = ".harness-install.json";
@@ -1626,7 +1716,7 @@ function mergeRootFiles(previous, current) {
1626
1716
  const prior = byPath.get(file.path);
1627
1717
  byPath.set(
1628
1718
  file.path,
1629
- prior ? {
1719
+ prior && file.change !== "displaced" ? {
1630
1720
  path: file.path,
1631
1721
  change: prior.change === "created" ? "created" : file.change,
1632
1722
  notes: [.../* @__PURE__ */ new Set([...prior.notes, ...file.notes])]
@@ -1666,7 +1756,7 @@ function mergeAssets(previous, current) {
1666
1756
  ];
1667
1757
  }
1668
1758
  function hashContent(content) {
1669
- return createHash("sha256").update(content, "utf8").digest("hex");
1759
+ return createHash2("sha256").update(content, "utf8").digest("hex");
1670
1760
  }
1671
1761
  function isHarnessOwned(baseline, key, current) {
1672
1762
  const recorded = baseline.get(key);
@@ -1712,26 +1802,45 @@ function migrateAwayLegacyLog(projectDir) {
1712
1802
  const legacy = legacyInstallLogPath(projectDir);
1713
1803
  if (!existsSync3(legacy)) return;
1714
1804
  try {
1715
- rmSync(legacy);
1805
+ rmSync2(legacy);
1716
1806
  } catch {
1717
1807
  }
1718
1808
  }
1719
1809
  function readInstallLog(projectDir) {
1810
+ return readInstallLogStatus(projectDir).log;
1811
+ }
1812
+ function readInstallLogStatus(projectDir) {
1720
1813
  const path2 = [installLogPath(projectDir), legacyInstallLogPath(projectDir)].find(
1721
1814
  (p) => existsSync3(p)
1722
1815
  );
1723
- if (!path2) return null;
1816
+ if (!path2) return { status: "missing", log: null };
1817
+ let parsed;
1724
1818
  try {
1725
- const parsed = JSON.parse(readFileSync3(path2, "utf8"));
1726
- if (Array.isArray(parsed.assets)) {
1727
- parsed.assets = parsed.assets.map(
1728
- (a) => a.method === "npm-global" ? { ...a, method: "npm" } : a
1729
- );
1730
- }
1731
- return parsed;
1819
+ parsed = JSON.parse(readFileSync3(path2, "utf8"));
1732
1820
  } catch {
1733
- return null;
1821
+ return { status: "corrupted", log: null };
1822
+ }
1823
+ if (!isValidInstallLogShape(parsed)) return { status: "corrupted", log: null };
1824
+ const log = parsed;
1825
+ if (Array.isArray(log.assets)) {
1826
+ log.assets = log.assets.map(
1827
+ (a) => a.method === "npm-global" ? { ...a, method: "npm" } : a
1828
+ );
1734
1829
  }
1830
+ return { status: "ok", log };
1831
+ }
1832
+ function isValidInstallLogShape(value) {
1833
+ if (typeof value !== "object" || value === null) return false;
1834
+ const v = value;
1835
+ const spec = v.spec;
1836
+ if (typeof spec !== "object" || spec === null) return false;
1837
+ if (!Array.isArray(spec.tracks)) return false;
1838
+ if (!Array.isArray(v.assets)) return false;
1839
+ if (typeof v.templates !== "object" || v.templates === null) return false;
1840
+ return true;
1841
+ }
1842
+ function corruptedInstallLogMessage(projectDir) {
1843
+ return `install log is corrupted at ${installLogPath(projectDir)} \u2014 run install --reinstall to rebuild it`;
1735
1844
  }
1736
1845
  function installLogPath(projectDir) {
1737
1846
  return join3(projectDir, INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
@@ -1752,7 +1861,6 @@ function buildAssetSpec(ctx) {
1752
1861
  }
1753
1862
  var all = () => true;
1754
1863
  var dev = (s) => hasDevTrack(s.tracks);
1755
- var ui = (s) => hasUiTrack(s.tracks);
1756
1864
  var onTracks = (pattern) => (s) => anyTrack(s.tracks, pattern);
1757
1865
  var COMMON_RULES = ["git-policy", "change-management", "doc-governance"];
1758
1866
  var DEV_RULES = ["test-policy", "ship-checklist"];
@@ -1808,11 +1916,63 @@ var RETIRED_AGENTS = [
1808
1916
  { id: "silent-failure-hunter", instead: "Claude Code \uC758 `/code-review` \uAC00 \uAC19\uC740 \uC77C\uC744 \uD55C\uB2E4" },
1809
1917
  { id: "build-error-resolver", instead: "\uBAA8\uB378\uC774 \uAE30\uBCF8\uC73C\uB85C \uD558\uB294 \uC77C\uC774\uB77C \uB300\uCCB4 \uC790\uC0B0\uC774 \uC5C6\uB2E4" }
1810
1918
  ];
1811
- var RETIRED_AGENT_IDS = RETIRED_AGENTS.map((a) => a.id);
1919
+ var RETIRED_PATHS = [
1920
+ // rules (23)
1921
+ "rules/api-contract.md",
1922
+ "rules/benchmark-parity.md",
1923
+ "rules/code-style.md",
1924
+ "rules/commit-policy.md",
1925
+ "rules/data-analysis.md",
1926
+ "rules/database.md",
1927
+ "rules/design-workflow.md",
1928
+ "rules/ecc-git-workflow.md",
1929
+ "rules/ecc-performance-common.md",
1930
+ "rules/ecc-security-common.md",
1931
+ "rules/ecc-testing.md",
1932
+ "rules/error-handling.md",
1933
+ "rules/gates-taxonomy.md",
1934
+ "rules/gsd-planner-antipatterns.md",
1935
+ "rules/htmx.md",
1936
+ "rules/model-routing.md",
1937
+ "rules/nextjs.md",
1938
+ "rules/no-false-ship.md",
1939
+ "rules/playwright-launch.md",
1940
+ "rules/pyside6.md",
1941
+ "rules/seo.md",
1942
+ "rules/shadcn.md",
1943
+ "rules/tauri.md",
1944
+ // agents (5)
1945
+ "agents/build-error-resolver.md",
1946
+ "agents/code-reviewer.md",
1947
+ "agents/plan-checker.md",
1948
+ "agents/security-reviewer.md",
1949
+ "agents/silent-failure-hunter.md",
1950
+ // commands/uzys (7)
1951
+ "commands/uzys/auto.md",
1952
+ "commands/uzys/build.md",
1953
+ "commands/uzys/plan.md",
1954
+ "commands/uzys/review.md",
1955
+ "commands/uzys/ship.md",
1956
+ "commands/uzys/spec.md",
1957
+ "commands/uzys/test.md",
1958
+ // hooks (10)
1959
+ "hooks/agentshield-gate.sh",
1960
+ "hooks/checkpoint-snapshot.sh",
1961
+ "hooks/codebase-map.sh",
1962
+ "hooks/gate-check.sh",
1963
+ "hooks/hito-counter.sh",
1964
+ "hooks/karpathy-gate.sh",
1965
+ "hooks/mcp-pre-exec.sh",
1966
+ "hooks/spec-drift-check.sh",
1967
+ "hooks/task-brief-nudge.sh",
1968
+ "hooks/uncommitted-check.sh"
1969
+ ];
1970
+ var RETIRED_AGENT_IDS = RETIRED_PATHS.filter(
1971
+ (p) => p.startsWith("agents/")
1972
+ ).map((p) => p.slice("agents/".length).replace(/\.md$/, ""));
1812
1973
  var ALWAYS_HOOKS = ["session-start.sh", "protect-files.sh"];
1813
1974
  var COMMON_SKILL_DIRS = [];
1814
1975
  var DEV_SKILL_DIRS = [];
1815
- var UI_SKILL_DIRS = ["ui-visual-review"];
1816
1976
  function isCliNeutralTarget(target) {
1817
1977
  return target.startsWith(`${INSTALL_LOG_DIR}/`);
1818
1978
  }
@@ -1887,7 +2047,14 @@ function buildManifest(spec) {
1887
2047
  source: `skills/${sd}`,
1888
2048
  target: `.claude/skills/${sd}`,
1889
2049
  type: "dir",
1890
- applies: (s) => (s.selectedInternalSkills ?? []).includes(sd)
2050
+ // #602 — selectedInternalSkills 가 SSOT(위저드 해제·--without 반영). `tracks` 만 담긴
2051
+ // 스펙(listBaselineTargets·위저드 1단계 등)에서는 카탈로그 조건으로 판정한다 —
2052
+ // buildAssetSpec 와 같은 isAssetSelected 라 두 번 적는 사본이 아니라 같은 원천이다.
2053
+ applies: (s) => s.selectedInternalSkills !== void 0 ? s.selectedInternalSkills.includes(sd) : (
2054
+ // #602 폴백은 트랙 추종 번들 스킬 한정 — 카탈로그 옵트인(compaction-handoff 등)은
2055
+ // 선택 목록 없이는 깔리지 않는다(위저드 해제·--without 의 SSOT 를 우회하는 금이 된다).
2056
+ sd === "ui-visual-review" && isAssetSelected(sd, { tracks: s.tracks, options: { withCodexTrust: false } })
2057
+ )
1891
2058
  });
1892
2059
  }
1893
2060
  for (const sd of DEV_SKILL_DIRS) {
@@ -1898,14 +2065,6 @@ function buildManifest(spec) {
1898
2065
  applies: dev
1899
2066
  });
1900
2067
  }
1901
- for (const sd of UI_SKILL_DIRS) {
1902
- m.push({
1903
- source: `skills/${sd}`,
1904
- target: `.claude/skills/${sd}`,
1905
- type: "dir",
1906
- applies: ui
1907
- });
1908
- }
1909
2068
  for (const h of ALWAYS_HOOKS) {
1910
2069
  m.push({
1911
2070
  source: `hooks/${h}`,
@@ -2001,10 +2160,11 @@ export {
2001
2160
  copyDir,
2002
2161
  backupDir,
2003
2162
  copyBackupDir,
2004
- backupFileIfChanged,
2005
2163
  backupFile,
2006
2164
  listFilesRecursive,
2007
2165
  ensureProjectSkeleton,
2166
+ dirTreesIdentical,
2167
+ backupIfLossyUtf8,
2008
2168
  INSTALL_LOG_FILENAME,
2009
2169
  INSTALL_LOG_DIR,
2010
2170
  POLICY_DIRS,
@@ -2017,17 +2177,19 @@ export {
2017
2177
  collectPolicyHashes,
2018
2178
  writeInstallLog,
2019
2179
  readInstallLog,
2180
+ readInstallLogStatus,
2181
+ corruptedInstallLogMessage,
2020
2182
  installLogPath,
2021
2183
  legacyInstallLogPath,
2022
2184
  buildAssetSpec,
2023
2185
  resolveRules,
2024
- ALL_RULES,
2025
2186
  TRACK_AGENTS,
2026
2187
  RETIRED_AGENTS,
2188
+ RETIRED_PATHS,
2027
2189
  RETIRED_AGENT_IDS,
2028
2190
  isCliNeutralTarget,
2029
2191
  buildManifest,
2030
2192
  CATEGORIES,
2031
2193
  CATEGORY_TITLES
2032
2194
  };
2033
- //# sourceMappingURL=chunk-MBRLCU5O.js.map
2195
+ //# sourceMappingURL=chunk-5T6MDT67.js.map