@uzysjung/agent-harness 26.162.1 → 26.164.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.
@@ -103,7 +103,7 @@ function globToRegex(pattern) {
103
103
  }
104
104
 
105
105
  // src/external-assets.ts
106
- var DEV_TRACKS_WITH_STACK = TRACKS.filter((t) => t !== "base" && hasDevTrack([t]));
106
+ var UI_TRACKS = TRACKS.filter((t) => hasUiTrack([t]));
107
107
  var CSR_SSR_NEXTJS_FULL = [
108
108
  "csr-supabase",
109
109
  "csr-fastify",
@@ -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",
@@ -379,15 +395,34 @@ var EXTERNAL_ASSETS = [
379
395
  pluginId: "railway@railway-skills"
380
396
  }
381
397
  },
398
+ // #709 (ADR-101) — Railway MCP 서버의 **선택** 경로. 서버 정의는 트랙 표(`templates/track-mcp-map.tsv`)의 같은 이름
399
+ // 행 하나다(key === 행 이름). 그 행의 패턴이 기본 트랙(csr-* · ssr-htmx · full)을 정하고, 이 항목은 그 밖의 트랙
400
+ // (ssr-nextjs 등)에서 위저드 체크 · `--with` 로 고르게 한다. experimental 이라 조건으로는 미리 체크되지 않는다.
401
+ // 상류 repo 는 archived — Railway 의 현행 경로(`railway mcp`)로 행 명령을 바꾸는 일은 별도 이슈.
402
+ {
403
+ id: "railway-mcp-server",
404
+ tier: "experimental",
405
+ // railwayapp/railway-mcp-server 190★ · archived (월간 trust-tier-drift 가 REPO_OVERRIDE 로 감시)
406
+ description: "Railway MCP server (`npx -y @railway/mcp-server`) in `.mcp.json` \u2014 already on by default on csr-*, ssr-htmx and full; check it to add it on any other track (e.g. ssr-nextjs). Upstream repo is archived; Railway's current route is `railway mcp` (opt-in)",
407
+ category: "backend",
408
+ source: "railwayapp",
409
+ condition: { kind: "opt-in" },
410
+ // #709 리뷰 NOTE 3 — `.mcp.json` · Codex `[mcp_servers]` · `opencode.json` `mcp` 만 받는다. Antigravity 변환에는 MCP 가 없다
411
+ // (internal 기본값 = 4 CLI 는 과대 표기).
412
+ cliSupportOverride: ["claude", "codex", "opencode"],
413
+ method: { kind: "internal", key: "railway-mcp-server" }
414
+ },
382
415
  // === csr-supabase|full CLI ===
383
416
  {
384
417
  id: "vercel-cli",
385
418
  tier: "vetted",
386
419
  // vercel/vercel 15k
387
- description: "Vercel CLI \u2014 the real deploy tool as a devDependency (global under --scope global). Only if this project deploys to Vercel",
420
+ description: "Vercel CLI \u2014 the real deploy tool as a devDependency (global under --scope global). Only if this project deploys to Vercel. `vercel mcp` then wires Vercel's official MCP (OAuth) into Claude Code",
388
421
  category: "backend",
389
422
  source: "vercel",
390
- // 2026-08-02 사용자 결정: 트랙 기본 → opt-in (ADR-063)
423
+ // 2026-08-02 사용자 결정: 트랙 기본 → opt-in (ADR-063). #709 가 ssr-nextjs 기본으로 바꿨다가 #715 에서 되돌렸다 —
424
+ // 설치자 package.json 에 의존성을 더하고(에이전트는 npx 로 쓸 수 있다) login 없이는 쓸 것이 없으며 핀이 낡는다.
425
+ // Next.js 의 Vercel 기본은 공식 MCP(#711)로 채운다.
391
426
  condition: { kind: "opt-in" },
392
427
  method: { kind: "npm", pkg: "vercel", version: "54.17.3" }
393
428
  },
@@ -417,6 +452,7 @@ var EXTERNAL_ASSETS = [
417
452
  // === UI tracks (csr-*|ssr-*|full) ===
418
453
  // v26.92.0 — frontend-design (Anthropic official).
419
454
  // 사용자 결정: has-dev-track 기본추천 (모든 개발 트랙, executive 제외).
455
+ // #709 (ADR-101) — UI 가 있는 트랙만(`UI_TRACKS`). data · tooling 은 미리 체크하지 않는다.
420
456
  // category=frontend (UI 자산, wizard 그룹).
421
457
  //
422
458
  // 2026-08-26 (#344) — **plugin → skill.** 배달 방식이 도달 CLI 를 정한다(assetCliSupport):
@@ -432,7 +468,7 @@ var EXTERNAL_ASSETS = [
432
468
  description: "frontend-design \u2014 visual direction, typography, and anti-generic UI guidance (Anthropic official). For new UI or an explicit redesign; do not let it override an existing design system",
433
469
  category: "frontend",
434
470
  source: "anthropics",
435
- condition: { kind: "any-track", tracks: DEV_TRACKS_WITH_STACK },
471
+ condition: { kind: "any-track", tracks: UI_TRACKS },
436
472
  method: { kind: "skill", source: "anthropics/skills", skill: "frontend-design" }
437
473
  },
438
474
  // 2026-08-02 정비 — 프론트엔드 품질 3종 (사용자 지시). 전부 opt-in: frontend-design 이 기본
@@ -484,9 +520,11 @@ var EXTERNAL_ASSETS = [
484
520
  // found" 가 됐다(#420) — 지금은 SKILL.md 의 `name:`(`preline-theme-generator`)으로 맞춘다(실측
485
521
  // 2026-09-20: 옛 이름 exit 1 · 새 이름 설치 완료). 이름이 바뀌면 월 1회 catalog-verify 가 잡는다.
486
522
  //
487
- // 같은 날 조사한 flowbite 는 **넣지 않는다**: 에이전트용 제공물이 MCP 서버뿐인데, 이 저장소의
488
- // `.mcp.json` 조립은 트랙 조건만 읽어 opt-in 경로가 없다 — 넣으면 해당 트랙 전원에게 항상
523
+ // 같은 날 조사한 flowbite 는 **넣지 않는다**: 에이전트용 제공물이 MCP 서버뿐인데, 그때 이 저장소의
524
+ // `.mcp.json` 조립은 트랙 조건만 읽어 opt-in 경로가 없었다 — 넣으면 해당 트랙 전원에게 항상
489
525
  // 켜진다. 안 쓰는 사람에게 MCP 툴 스키마만큼의 상주 비용을 물리는 형태라 기각(사용자 확정).
526
+ // #709 (ADR-101) 부터는 카탈로그 `internal` 항목으로 opt-in 경로가 있다(`railway-mcp-server`). flowbite 는
527
+ // 그 경로로 다시 심사할 수 있다 — 이 변경은 넣지 않는다.
490
528
  {
491
529
  id: "preline",
492
530
  tier: "vetted",
@@ -870,6 +908,10 @@ var DEV_METHOD_SKILL_IDS = [
870
908
  ];
871
909
  var INTERNAL_BUNDLED_SKILL_IDS = [
872
910
  ...DEV_METHOD_SKILL_IDS,
911
+ // #602 — 트랙을 따라가는 번들 스킬(csr-*·ssr-*·full). selectedInternalSkills 로 흘러야
912
+ // .agents/skills(비-Claude 자리)에도 깔린다 — manifest 의 UI_SKILL_DIRS 배선은 .claude/ 만
913
+ // 알아 TRACKS.md 와 어긋났다. 조건은 buildAssetSpec 이 UI 트랙으로 건다.
914
+ "ui-visual-review",
873
915
  "north-star",
874
916
  "gh-issue-workflow",
875
917
  "objective-brief",
@@ -975,470 +1017,265 @@ function filterApplicableAssets(assets, ctx) {
975
1017
  return assets.filter((a) => shouldInstallAsset(a, ctx));
976
1018
  }
977
1019
 
978
- // src/context-cost.ts
1020
+ // src/manifest.ts
979
1021
  init_esm_shims();
980
- import { existsSync, readFileSync } from "fs";
981
- import { join, resolve } from "path";
982
- import { fileURLToPath as fileURLToPath2 } from "url";
983
1022
 
984
- // src/project-claude-merge.ts
1023
+ // src/install-log.ts
985
1024
  init_esm_shims();
986
- var HARNESS_ANCHOR_FILE = "CLAUDE-uzys-harness.md";
987
- var HARNESS_IMPORT_LINE = `@${HARNESS_ANCHOR_FILE}`;
988
- var IMPORT_MARKER_START = "<!-- uzys-harness:import:start -->";
989
- var IMPORT_MARKER_END = "<!-- uzys-harness:import:end -->";
990
- var CONTINUOUS_SKILLS_HEADING = "## Skills that apply continuously";
991
- function renderContinuousSkillsNote(selectedInternalSkills) {
992
- const lines = CONTINUOUS_SKILLS.filter((s) => selectedInternalSkills.includes(s.id)).map(
993
- (s) => `- \`${s.id}\` \u2014 ${s.whenToApply}.`
994
- );
995
- if (lines.length === 0) return "";
996
- return [
997
- CONTINUOUS_SKILLS_HEADING,
998
- "",
999
- "These installed skills apply to every response or delegation; no prompt ever looks like their job, so open them from this list:",
1000
- "",
1001
- ...lines
1002
- ].join("\n");
1003
- }
1004
- function withContinuousSkillsNote(scaffold, selectedInternalSkills) {
1005
- const note = renderContinuousSkillsNote(selectedInternalSkills);
1006
- return note ? `${scaffold}
1025
+ import { createHash as createHash2 } from "crypto";
1026
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, statSync, writeFileSync } from "fs";
1027
+ import { dirname as dirname3, join as join4 } from "path";
1007
1028
 
1008
- ${note}` : scaffold;
1009
- }
1010
- function importBlock(continuousSkills) {
1011
- const note = renderContinuousSkillsNote(continuousSkills);
1012
- const body = note ? `${HARNESS_IMPORT_LINE}
1013
-
1014
- ${note}` : HARNESS_IMPORT_LINE;
1015
- return `${IMPORT_MARKER_START}
1016
- ${body}
1017
- ${IMPORT_MARKER_END}`;
1018
- }
1019
- var TRACK_DISPLAY_NAMES = {
1020
- base: "Base",
1021
- tooling: "Tooling",
1022
- "csr-supabase": "CSR Supabase",
1023
- "csr-fastify": "CSR Fastify",
1024
- "csr-fastapi": "CSR FastAPI",
1025
- "ssr-htmx": "SSR HTMX",
1026
- "ssr-nextjs": "SSR Next.js",
1027
- data: "Data",
1028
- executive: "Executive",
1029
- full: "Full",
1030
- "project-management": "Project Management",
1031
- "growth-marketing": "Growth Marketing"
1032
- };
1033
- var FULL_EXPANSION = TRACKS.filter((t) => t !== "full");
1034
- var FILL_SECTIONS = [
1035
- "identity",
1036
- "stack",
1037
- "architecture",
1038
- "installed-assets",
1039
- "boundaries",
1040
- "verify"
1041
- ];
1042
- var FILL_SPECS = {
1043
- identity: {
1044
- title: "Identity & Purpose",
1045
- prompt: `Replace the H1 title above with this project's real name, then state in 1-2 plain sentences what it does, who uses it, and why it exists. Sources: README.md, the package.json / pyproject.toml "description", docs/. Do NOT describe the harness itself. Delete this comment when done.`,
1046
- placeholder: "what this project is, who it is for, and why it exists"
1029
+ // package.json
1030
+ var package_default = {
1031
+ name: "@uzysjung/agent-harness",
1032
+ version: "26.164.0",
1033
+ description: "Curate vetted AI-coding skills & plugins by your tech stack \u2014 install only what you need, across Claude Code, Codex, OpenCode & Antigravity",
1034
+ type: "module",
1035
+ publishConfig: {
1036
+ access: "public"
1047
1037
  },
1048
- stack: {
1049
- title: "Stack & Commands",
1050
- prompt: "Replace the list below with this project's REAL stack. Inspect package.json / pyproject.toml / go.mod / Cargo.toml / Gemfile + lockfiles and any Makefile/justfile/package scripts. List the language(s)+versions, framework(s), package manager, and the exact install/build/test/run commands. Verify each command exists before writing it \u2014 never guess. Delete lines that do not apply, then delete this comment.",
1051
- placeholder: "languages, runtimes, package manager, and the install/build/test/run commands"
1038
+ engines: {
1039
+ node: ">=20.12.0"
1052
1040
  },
1053
- architecture: {
1054
- title: "Architecture & Layout",
1055
- prompt: "Map this repository. List each top-level source directory and what it holds, the entry point(s), the 3-5 files a newcomer reads first, and how data/requests flow between the main layers. View the real tree (e.g. `git ls-files | sed 's#/.*##' | sort -u`) \u2014 do not assume a framework's conventional layout. Flag any generated/vendored directories that must never be hand-edited. If this is a single small module, say so in one line. Delete this comment when done.",
1056
- placeholder: "where things live, the entry points, and how data flows between layers"
1041
+ bin: {
1042
+ "agent-harness": "./dist/index.js"
1057
1043
  },
1058
- "installed-assets": {
1059
- title: "Installed Harness Assets",
1060
- prompt: "List the harness assets installed in this project (rules / skills / agents / commands) and add one line each on when to reach for it here. Verify against .claude/rules/*.md, .claude/skills/, .claude/agents/, .claude/commands/ (or this CLI's equivalent) and list ONLY assets whose file exists on disk \u2014 do not invent any. Do NOT restate the harness's working principles (they live in this project's harness anchor and rules layer) \u2014 cross-reference them instead. Delete this comment when done.",
1061
- placeholder: "the installed rules/skills/agents/commands and when to use each"
1044
+ files: [
1045
+ "dist",
1046
+ "templates",
1047
+ "README.md",
1048
+ "LICENSE"
1049
+ ],
1050
+ scripts: {
1051
+ build: "tsup",
1052
+ dev: "tsup --watch",
1053
+ "gen:compat": "npm run build && node scripts/gen-compatibility.mjs",
1054
+ test: "vitest run",
1055
+ "test:watch": "vitest",
1056
+ "test:coverage": "vitest run --coverage",
1057
+ typecheck: "tsc --noEmit",
1058
+ lint: "biome check src tests",
1059
+ "lint:fix": "biome check --write src tests",
1060
+ format: "biome format --write src tests",
1061
+ ci: "npm run typecheck && npm run lint && npm run test:coverage && npm run build",
1062
+ prepare: "[ -d dist ] || npm run build",
1063
+ "cost:report": "npm run build && node scripts/context-cost-report.mjs",
1064
+ "cost:baseline": "npm run build && node scripts/context-cost-baseline.mjs",
1065
+ "assets:history": "bash scripts/asset-history.sh",
1066
+ "release:audit": "node scripts/release-audit.mjs",
1067
+ security: "npm audit --omit=dev --audit-level=high && npx --yes ecc-agentshield@1.4.0 scan --baseline security-baseline.json --gate",
1068
+ "security:baseline": "npx --yes ecc-agentshield@1.4.0 scan --save-baseline security-baseline.json"
1062
1069
  },
1063
- boundaries: {
1064
- title: "Boundaries \u2014 Always / Ask First / Never",
1065
- prompt: `Fill the Always / Ask First / Never lists with this repo's real red lines. Read the CI config, CODEOWNERS, .gitignore, and release/deploy scripts. Never must include secrets, generated files, force-push, and direct commits to the default branch, plus any repo-specific "do not touch" directories. Every entry must be specific and enforceable here. Delete this comment when done.`,
1066
- placeholder: "what to always do, ask before doing, and never do in this repo"
1070
+ dependencies: {
1071
+ "@clack/prompts": "^1.3.0",
1072
+ cac: "^7.0.0",
1073
+ "smol-toml": "^1.9.0"
1067
1074
  },
1068
- verify: {
1069
- title: "Verification Gate",
1070
- prompt: 'State the single command (or short sequence) that PROVES a change is safe here \u2014 the test/lint/typecheck/build gate you run before committing \u2014 plus the coverage/CI threshold and what "done" means. Sources: package scripts, CI workflow files, CONTRIBUTING. An agent must be able to self-verify without guessing. Delete this comment when done.',
1071
- placeholder: "the single command that proves a change is safe, and the done bar"
1075
+ devDependencies: {
1076
+ "@biomejs/biome": "^2.4.13",
1077
+ "@types/node": "^25.6.0",
1078
+ "@vitest/coverage-v8": "^4.1.11",
1079
+ tsup: "^8.3.0",
1080
+ typescript: "^5.6.0",
1081
+ vitest: "^4.1.11"
1082
+ },
1083
+ license: "MIT",
1084
+ repository: {
1085
+ type: "git",
1086
+ url: "git+https://github.com/uzysjung/uzys-agent-harness.git"
1072
1087
  }
1073
1088
  };
1074
- function anchorLine(surface) {
1075
- if (surface === "claude") {
1076
- return `> The working principles live in this project's harness anchor \`${HARNESS_ANCHOR_FILE}\`, imported at the bottom of this file. Everything above that import is **project-specific context only**.`;
1077
- }
1078
- const self = surface === "antigravity-rule" ? "`.agents/rules/uzys-harness.md`" : "`AGENTS.md`";
1079
- return `> **This file (${self}) is the harness anchor** \u2014 it carries the working principles in full, further down. Only the \`## \u2026\` sections immediately below are project-specific context for you to fill in.`;
1080
- }
1081
- function scaffoldBanner(surface) {
1082
- return [
1083
- "> \u2699\uFE0F **SCAFFOLD \u2014 not filled in yet.** The sections below are a fill-in template for THIS project, not verified facts.",
1084
- "> To fill: open this file and paste each `<!-- FILL: \u2026 -->` comment's instruction into your coding agent (e.g. Claude Code) \u2014 it will inspect the real repo and write the section. You can also fill them by hand; the comments are the instructions.",
1085
- anchorLine(surface)
1086
- ].join("\n");
1087
- }
1088
- function renderFillScaffold(surface = "claude") {
1089
- const blocks = FILL_SECTIONS.map((id) => {
1090
- const spec = FILL_SPECS[id];
1091
- return `## ${spec.title}
1092
-
1093
- <!-- FILL:${id} \u2014 ${spec.prompt} -->
1094
1089
 
1095
- _(not filled yet \u2014 ${spec.placeholder})_`;
1096
- });
1097
- return `${scaffoldBanner(surface)}
1098
-
1099
- ${blocks.join("\n\n")}`;
1090
+ // src/adapters/shared-files.ts
1091
+ init_esm_shims();
1092
+ var SHARED_FILES = {
1093
+ "CLAUDE.md": { adapter: "marker-md", prefix: "claude-md:" },
1094
+ "AGENTS.md": { adapter: "marker-md", prefix: "agents-md:" },
1095
+ ".claude/settings.json": { adapter: "json-keys", prefix: "settings:" },
1096
+ ".mcp.json": { adapter: "json-keys", prefix: "mcp:", strip: "mcpServers." },
1097
+ "opencode.json": { adapter: "json-keys", prefix: "opencode:" },
1098
+ ".codex/config.toml": { adapter: "toml-region", prefix: "codex:" },
1099
+ ".gitignore": { adapter: "lines", prefix: "gitignore:" }
1100
+ };
1101
+ var KEY_ID_PREFIXES = Object.values(SHARED_FILES).map(
1102
+ (f) => f.prefix
1103
+ );
1104
+ function isKeyId(id) {
1105
+ return KEY_ID_PREFIXES.some((p) => id.startsWith(p));
1100
1106
  }
1101
- function mergeProjectClaude(opts) {
1102
- const expanded = expandTracks(opts.tracks);
1103
- const trackList = expanded.map((t) => TRACK_DISPLAY_NAMES[t]).join(", ");
1104
- const header = `# ${opts.projectName}
1105
-
1106
- > Active track(s): ${trackList}`;
1107
- return `${header}
1108
-
1109
- ${opts.projectContext ?? renderFillScaffold()}
1110
- `;
1107
+ function sharedPathOfKeyId(id) {
1108
+ for (const [path2, file] of Object.entries(SHARED_FILES))
1109
+ if (id.startsWith(file.prefix)) return path2;
1110
+ return null;
1111
1111
  }
1112
- function upsertHarnessImport(existing, opts) {
1113
- const block = importBlock(opts.continuousSkills ?? []);
1114
- if (existing === null) {
1115
- return `${mergeProjectClaude(opts)}
1116
- ${block}
1117
- `;
1112
+
1113
+ // src/cli-targets.ts
1114
+ init_esm_shims();
1115
+ var CLI_BASE_SORT_ORDER = {
1116
+ claude: 0,
1117
+ codex: 1,
1118
+ opencode: 2,
1119
+ antigravity: 3
1120
+ };
1121
+ function parseCliTargets(input) {
1122
+ const items = normalizeInput(input);
1123
+ if (items.length === 0) {
1124
+ return { ok: true, targets: ["claude"], warnings: [] };
1118
1125
  }
1119
- const start = existing.indexOf(IMPORT_MARKER_START);
1120
- if (start !== -1) {
1121
- const endAt = existing.indexOf(IMPORT_MARKER_END, start);
1122
- if (endAt === -1) return existing;
1123
- const current = existing.slice(start, endAt + IMPORT_MARKER_END.length);
1124
- if (current === block) return existing;
1125
- return `${existing.slice(0, start)}${block}${existing.slice(endAt + IMPORT_MARKER_END.length)}`;
1126
+ const collected = /* @__PURE__ */ new Set();
1127
+ const warnings = [];
1128
+ for (const item of items) {
1129
+ if (!isCliBase(item)) {
1130
+ let hint = "";
1131
+ if (item === "both") {
1132
+ hint = "\n v0.8.0 removed 'both' alias. Use --cli claude --cli codex.";
1133
+ } else if (item === "all") {
1134
+ hint = "\n v0.8.0 removed 'all' alias. Use --cli claude --cli codex --cli opencode.";
1135
+ } else if (item.includes(",")) {
1136
+ hint = "\n Tip: comma-separated not supported. Use --cli A --cli B for multiple.";
1137
+ }
1138
+ return {
1139
+ ok: false,
1140
+ targets: ["claude"],
1141
+ warnings,
1142
+ error: `Invalid --cli value: ${item}. Must be one of: ${CLI_BASES.join(" | ")}${hint}`
1143
+ };
1144
+ }
1145
+ collected.add(item);
1126
1146
  }
1127
- if (hasHarnessImport(existing)) {
1128
- return existing;
1147
+ const targets = [...collected].sort((a, b) => CLI_BASE_SORT_ORDER[a] - CLI_BASE_SORT_ORDER[b]);
1148
+ return { ok: true, targets, warnings };
1149
+ }
1150
+ function normalizeInput(input) {
1151
+ if (input === void 0 || input === null) return [];
1152
+ if (typeof input === "string") {
1153
+ const trimmed = input.trim();
1154
+ return trimmed === "" ? [] : [trimmed];
1129
1155
  }
1130
- const body = existing.endsWith("\n") ? existing : `${existing}
1131
- `;
1132
- return `${body}
1133
- ${block}
1134
- `;
1156
+ return input.filter((s) => typeof s === "string" && s.trim() !== "").map((s) => s.trim());
1135
1157
  }
1136
- 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}`;
1158
+ function targetsInclude(targets, base) {
1159
+ return targets.includes(base);
1144
1160
  }
1145
- function hasHarnessImport(text) {
1146
- if (text.includes(IMPORT_MARKER_START)) return true;
1147
- let inFence = false;
1148
- for (const line of text.split("\n")) {
1149
- if (/^\s*(?:```|~~~)/.test(line)) {
1150
- inFence = !inFence;
1161
+
1162
+ // src/fs-ops.ts
1163
+ init_esm_shims();
1164
+ import { createHash } from "crypto";
1165
+ import {
1166
+ copyFileSync,
1167
+ existsSync,
1168
+ constants as fsConstants,
1169
+ mkdirSync,
1170
+ readdirSync,
1171
+ readFileSync,
1172
+ readlinkSync as readlinkSync2,
1173
+ realpathSync as realpathSync2,
1174
+ renameSync,
1175
+ rmdirSync,
1176
+ rmSync,
1177
+ symlinkSync
1178
+ } from "fs";
1179
+ import { dirname as dirname2, join as join2 } from "path";
1180
+
1181
+ // src/outside-project.ts
1182
+ init_esm_shims();
1183
+ import { lstatSync, readlinkSync, realpathSync } from "fs";
1184
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path";
1185
+ function isOutsideProject(projectDir, target) {
1186
+ const root = resolveWriteTarget(projectDir) ?? projectDir;
1187
+ const rel = relative(root, target);
1188
+ return rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel);
1189
+ }
1190
+ function resolveWriteTarget(abs) {
1191
+ const tail = [];
1192
+ const hops = /* @__PURE__ */ new Set();
1193
+ let path2 = abs;
1194
+ for (; ; ) {
1195
+ try {
1196
+ return join(realpathSync(path2), ...tail);
1197
+ } catch {
1198
+ }
1199
+ let isLink = false;
1200
+ try {
1201
+ const st = lstatSync(path2, { throwIfNoEntry: false });
1202
+ if (st !== void 0 && !st.isSymbolicLink()) return null;
1203
+ isLink = st !== void 0;
1204
+ } catch {
1205
+ return null;
1206
+ }
1207
+ if (isLink) {
1208
+ if (hops.has(path2)) return null;
1209
+ hops.add(path2);
1210
+ path2 = resolve(dirname(path2), readlinkSync(path2));
1151
1211
  continue;
1152
1212
  }
1153
- if (!inFence && line.trim() === HARNESS_IMPORT_LINE) return true;
1213
+ const parent = dirname(path2);
1214
+ if (parent === path2) return null;
1215
+ tail.unshift(basename(path2));
1216
+ path2 = parent;
1154
1217
  }
1155
- return false;
1156
1218
  }
1157
- function expandTracks(tracks) {
1158
- if (tracks.includes("full")) {
1159
- return FULL_EXPANSION;
1219
+ function outsideProjectTarget(projectDir, abs) {
1220
+ const target = resolveWriteTarget(abs);
1221
+ if (target === null || !isOutsideProject(projectDir, target)) return null;
1222
+ const rel = relative(projectDir, abs);
1223
+ const parts = rel.split(sep);
1224
+ let link = rel;
1225
+ let linkTarget = target;
1226
+ for (let i = 1; i <= parts.length; i++) {
1227
+ const prefix = join(projectDir, ...parts.slice(0, i));
1228
+ const st = lstatSync(prefix, { throwIfNoEntry: false });
1229
+ if (st?.isSymbolicLink() !== true) continue;
1230
+ const resolved = resolveWriteTarget(prefix);
1231
+ if (resolved === null || !isOutsideProject(projectDir, resolved)) continue;
1232
+ link = parts.slice(0, i).join(sep);
1233
+ linkTarget = resolved;
1234
+ break;
1160
1235
  }
1161
- return tracks;
1236
+ const slash = (p) => p.split(sep).join("/");
1237
+ return { path: slash(rel), target, link: slash(link), linkTarget };
1162
1238
  }
1163
-
1164
- // src/context-cost.ts
1165
- var CHARS_PER_TOKEN = 4;
1166
- function estimateTokens(chars) {
1167
- return Math.ceil(chars / CHARS_PER_TOKEN);
1168
- }
1169
- function resolveBundleRoot() {
1170
- return resolve(fileURLToPath2(new URL(".", import.meta.url)), "..");
1239
+ function outsideLinkAt(projectDir, rel) {
1240
+ const abs = join(projectDir, rel);
1241
+ try {
1242
+ if (!lstatSync(abs, { throwIfNoEntry: false })?.isSymbolicLink()) return null;
1243
+ } catch {
1244
+ return null;
1245
+ }
1246
+ return outsideProjectTarget(projectDir, abs);
1171
1247
  }
1172
- function extractFrontmatter(content) {
1173
- const m = content.match(/^?---\r?\n([\s\S]*?)\r?\n---/);
1174
- return m?.[1] ?? null;
1248
+ function createOutsideGuard(projectDir) {
1249
+ const seen = /* @__PURE__ */ new Map();
1250
+ return {
1251
+ skip(abs) {
1252
+ const hit = outsideProjectTarget(projectDir, abs);
1253
+ if (hit === null) return false;
1254
+ if (!seen.has(hit.path)) seen.set(hit.path, hit);
1255
+ return true;
1256
+ },
1257
+ list: () => [...seen.values()]
1258
+ };
1175
1259
  }
1176
- function assetDescriptorTokens(assetId, root = resolveBundleRoot()) {
1177
- const asset = EXTERNAL_ASSETS.find((a) => a.id === assetId);
1178
- if (!asset || asset.method.kind !== "internal") return null;
1179
- const skillMd = join(root, "templates", "skills", asset.method.key, "SKILL.md");
1180
- if (!existsSync(skillMd)) return null;
1181
- const fm = extractFrontmatter(readFileSync(skillMd, "utf8"));
1182
- return fm === null ? null : estimateTokens(fm.length);
1183
- }
1184
- function assetBodyTokens(assetId, root = resolveBundleRoot()) {
1185
- const asset = EXTERNAL_ASSETS.find((a) => a.id === assetId);
1186
- if (!asset || asset.method.kind !== "internal") return null;
1187
- const skillMd = join(root, "templates", "skills", asset.method.key, "SKILL.md");
1188
- if (!existsSync(skillMd)) return null;
1189
- const content = readFileSync(skillMd, "utf8");
1190
- const fm = extractFrontmatter(content);
1191
- if (fm === null) return estimateTokens(content.length);
1192
- const close = content.indexOf("\n---", content.indexOf(fm) + fm.length);
1193
- const body = close === -1 ? "" : content.slice(close + "\n---".length);
1194
- return estimateTokens(body.trim().length);
1195
- }
1196
- function assetCostRows(assetIds, root = resolveBundleRoot()) {
1197
- return assetIds.map((id) => ({
1198
- id,
1199
- descriptorTokens: assetDescriptorTokens(id, root),
1200
- bodyTokens: assetBodyTokens(id, root)
1201
- })).sort((a, b) => (b.bodyTokens ?? -1) - (a.bodyTokens ?? -1));
1202
- }
1203
- function makeResidentCost(base) {
1204
- return {
1205
- ...base,
1206
- perSkillDescriptor: base.perSkillDescriptor ?? {},
1207
- total: base.rules + base.projectClaudeMd + base.skillDescriptors + base.agentDescriptors,
1208
- directive: {
1209
- items: base.items.rules + base.items.claudeMd,
1210
- tokens: base.rules + base.projectClaudeMd
1211
- },
1212
- firing: {
1213
- items: base.items.skills + base.items.agents,
1214
- tokens: base.skillDescriptors + base.agentDescriptors
1215
- }
1216
- };
1217
- }
1218
- function fileTokens(path2) {
1219
- return existsSync(path2) ? estimateTokens(readFileSync(path2, "utf8").trim().length) : 0;
1220
- }
1221
- function descriptorTokens(path2) {
1222
- if (!existsSync(path2)) return 0;
1223
- const fm = extractFrontmatter(readFileSync(path2, "utf8"));
1224
- return fm === null ? 0 : estimateTokens(fm.length);
1225
- }
1226
- function landsOnDisk(target, cli) {
1227
- if (target.startsWith(".claude/agents/")) return cli.includes("claude");
1228
- return true;
1229
- }
1230
- function residentCost(entries, root = resolveBundleRoot()) {
1231
- const tpl = (source) => join(root, "templates", source);
1232
- const measured = (e) => e.file ?? tpl(e.source);
1233
- const perSkill = {};
1234
- let rules = 0;
1235
- let skillDescriptors = 0;
1236
- let agentDescriptors = 0;
1237
- let ruleItems = 0;
1238
- let skillItems = 0;
1239
- let agentItems = 0;
1240
- for (const e of entries) {
1241
- if (e.target.startsWith(".claude/rules/")) {
1242
- rules += fileTokens(measured(e));
1243
- ruleItems += 1;
1244
- } else if (e.target.startsWith(".claude/agents/")) {
1245
- agentDescriptors += descriptorTokens(measured(e));
1246
- agentItems += 1;
1247
- } else if (e.target.startsWith(".claude/skills/")) {
1248
- const t = descriptorTokens(join(measured(e), "SKILL.md"));
1249
- skillDescriptors += t;
1250
- skillItems += 1;
1251
- const id = /\.claude\/skills\/([^/]+)/.exec(e.target)?.[1];
1252
- if (id !== void 0) perSkill[id] = (perSkill[id] ?? 0) + t;
1253
- }
1254
- }
1255
- const harnessAnchor = fileTokens(join(root, "templates", "CLAUDE.md"));
1256
- const projectScaffold = estimateTokens(
1257
- withContinuousSkillsNote(
1258
- renderFillScaffold(),
1259
- CONTINUOUS_SKILLS.map((s) => s.id)
1260
- ).trim().length
1261
- );
1262
- const projectClaudeMd = harnessAnchor + projectScaffold;
1263
- const claudeMdItems = (harnessAnchor > 0 ? 1 : 0) + (projectScaffold > 0 ? 1 : 0);
1264
- return makeResidentCost({
1265
- rules,
1266
- projectClaudeMd,
1267
- skillDescriptors,
1268
- agentDescriptors,
1269
- perSkillDescriptor: perSkill,
1270
- items: {
1271
- rules: ruleItems,
1272
- skills: skillItems,
1273
- agents: agentItems,
1274
- claudeMd: claudeMdItems,
1275
- total: ruleItems + skillItems + agentItems + claudeMdItems
1276
- }
1277
- });
1278
- }
1279
- function summarizeContextCost(assetIds, root = resolveBundleRoot()) {
1280
- let measuredTokens = 0;
1281
- let measuredCount = 0;
1282
- let unmeasuredCount = 0;
1283
- for (const id of assetIds) {
1284
- const tokens = assetDescriptorTokens(id, root);
1285
- if (tokens === null) unmeasuredCount += 1;
1286
- else {
1287
- measuredTokens += tokens;
1288
- measuredCount += 1;
1289
- }
1290
- }
1291
- return { measuredTokens, measuredCount, unmeasuredCount };
1292
- }
1293
- function formatResidentCostLine(r, unmeasuredCount) {
1294
- if (r.total === 0) return null;
1295
- const parts = [
1296
- `directives ${r.directive.items} ~${r.directive.tokens}`,
1297
- `[rules ${r.items.rules} ~${r.rules} \xB7 CLAUDE.md ${r.items.claudeMd} ~${r.projectClaudeMd}]`,
1298
- `triggers ${r.firing.items} ~${r.firing.tokens}`,
1299
- `[skills ${r.items.skills} ~${r.skillDescriptors} \xB7 agents ${r.items.agents} ~${r.agentDescriptors}]`
1300
- ].join(" \xB7 ");
1301
- const external = unmeasuredCount > 0 ? ` \xB7 ${unmeasuredCount} external asset${unmeasuredCount === 1 ? "" : "s"} unmeasured` : "";
1302
- return `session-start context cost: ${r.items.total} items resident \xB7 ~${r.total} tokens/session (${parts}${external})`;
1303
- }
1304
- var RESIDENT_ROWS = [
1305
- {
1306
- // ADR-083 — 축 소계를 **행으로** 낸다. 소계가 없으면 읽는 사람이 네 줄을 머리로 더해야
1307
- // 하고, 그러면 "무엇을 줄여야 하나"가 표에서 안 보인다.
1308
- label: "\uC9C0\uC2DC\uBB38",
1309
- gap: 13,
1310
- count: (r) => r.directive.items,
1311
- tokens: (r) => r.directive.tokens
1312
- },
1313
- { label: " rules", gap: 12, count: (r) => r.items.rules, tokens: (r) => r.rules },
1314
- {
1315
- // 한 행에 두 파일(앵커 + 스캐폴드). 개수 열이 2 를 뱉어 그 사실을 드러낸다 — 라벨로
1316
- // 하나만 이름 붙이면 v26.140.0 이전과 같은 거짓 라벨이 된다.
1317
- label: " CLAUDE.md",
1318
- gap: 8,
1319
- count: (r) => r.items.claudeMd,
1320
- tokens: (r) => r.projectClaudeMd
1321
- },
1322
- {
1323
- label: "\uBC1C\uD654 \uD45C\uBA74",
1324
- gap: 10,
1325
- count: (r) => r.firing.items,
1326
- tokens: (r) => r.firing.tokens
1327
- },
1328
- {
1329
- label: " skill descriptors",
1330
- gap: 0,
1331
- count: (r) => r.items.skills,
1332
- tokens: (r) => r.skillDescriptors
1333
- },
1334
- {
1335
- label: " agent descriptors",
1336
- gap: 0,
1337
- count: (r) => r.items.agents,
1338
- tokens: (r) => r.agentDescriptors
1339
- }
1340
- ];
1341
- function formatResidentCostBlock(r) {
1342
- const n = (v) => String(v).padStart(3);
1343
- return [
1344
- " \uAC1C\uC218 \uD1A0\uD070",
1345
- ...RESIDENT_ROWS.map(
1346
- (row) => ` ${row.label}${" ".repeat(row.gap)}${n(row.count(r))}\uAC1C ~${row.tokens(r)}`
1347
- ),
1348
- " \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
1349
- ` \uC0C1\uC8FC \uD569\uACC4 ${n(r.items.total)}\uAC1C \uC0C1\uC8FC \xB7 ~${r.total} tokens/\uC138\uC158`,
1350
- "",
1351
- " \uC9C0\uC2DC\uBB38 = \uC6B0\uB9AC\uAC00 \uD56D\uC0C1 \uC77D\uD788\uAC8C \uB123\uC740 \uAC83. **\uC791\uC744\uC218\uB85D \uC88B\uC740 \uAC12\uC774 \uC544\uB2C8\uB2E4** \u2014 \uD310\uB2E8\uC740",
1352
- " '\uBD88\uD544\uC694\uD55C \uC9C0\uC2DC\uBB38\uC774 \uC788\uB098'\uB85C \uD55C\uB2E4. ratchet \uC740 \uC870\uC6A9\uD55C \uC99D\uAC00\uB9CC \uB9C9\uB294\uB2E4.",
1353
- " \uBC1C\uD654 \uD45C\uBA74 = \uC2A4\uD0AC\xB7\uC5D0\uC774\uC804\uD2B8\uAC00 \uBD88\uB9AC\uB824\uACE0 \uC0C1\uC8FC\uC2DC\uD0A4\uB294 descriptor. \uAE4E\uC73C\uBA74 \uC548 \uBD88\uB9B0\uB2E4."
1354
- ];
1355
- }
1356
-
1357
- // src/manifest.ts
1358
- init_esm_shims();
1359
-
1360
- // src/install-log.ts
1361
- init_esm_shims();
1362
- import { createHash } from "crypto";
1363
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync } from "fs";
1364
- import { dirname as dirname2, join as join3 } from "path";
1365
-
1366
- // src/cli-targets.ts
1367
- init_esm_shims();
1368
- var CLI_BASE_SORT_ORDER = {
1369
- claude: 0,
1370
- codex: 1,
1371
- opencode: 2,
1372
- antigravity: 3
1373
- };
1374
- function parseCliTargets(input) {
1375
- const items = normalizeInput(input);
1376
- if (items.length === 0) {
1377
- return { ok: true, targets: ["claude"], warnings: [] };
1378
- }
1379
- const collected = /* @__PURE__ */ new Set();
1380
- const warnings = [];
1381
- for (const item of items) {
1382
- if (!isCliBase(item)) {
1383
- let hint = "";
1384
- if (item === "both") {
1385
- hint = "\n v0.8.0 removed 'both' alias. Use --cli claude --cli codex.";
1386
- } else if (item === "all") {
1387
- hint = "\n v0.8.0 removed 'all' alias. Use --cli claude --cli codex --cli opencode.";
1388
- } else if (item.includes(",")) {
1389
- hint = "\n Tip: comma-separated not supported. Use --cli A --cli B for multiple.";
1390
- }
1391
- return {
1392
- ok: false,
1393
- targets: ["claude"],
1394
- warnings,
1395
- error: `Invalid --cli value: ${item}. Must be one of: ${CLI_BASES.join(" | ")}${hint}`
1396
- };
1397
- }
1398
- collected.add(item);
1399
- }
1400
- const targets = [...collected].sort((a, b) => CLI_BASE_SORT_ORDER[a] - CLI_BASE_SORT_ORDER[b]);
1401
- return { ok: true, targets, warnings };
1402
- }
1403
- function normalizeInput(input) {
1404
- if (input === void 0 || input === null) return [];
1405
- if (typeof input === "string") {
1406
- const trimmed = input.trim();
1407
- return trimmed === "" ? [] : [trimmed];
1408
- }
1409
- return input.filter((s) => typeof s === "string" && s.trim() !== "").map((s) => s.trim());
1410
- }
1411
- function targetsInclude(targets, base) {
1412
- return targets.includes(base);
1260
+ function mergeOutside(...lists) {
1261
+ const out = /* @__PURE__ */ new Map();
1262
+ for (const list of lists) for (const o of list) if (!out.has(o.path)) out.set(o.path, o);
1263
+ return [...out.values()];
1413
1264
  }
1414
1265
 
1415
1266
  // src/fs-ops.ts
1416
- init_esm_shims();
1417
- import {
1418
- copyFileSync,
1419
- cpSync,
1420
- existsSync as existsSync2,
1421
- constants as fsConstants,
1422
- mkdirSync,
1423
- readdirSync,
1424
- readFileSync as readFileSync2,
1425
- realpathSync,
1426
- renameSync,
1427
- rmdirSync
1428
- } from "fs";
1429
- import { dirname, join as join2 } from "path";
1430
1267
  function ensureDir(path2) {
1431
1268
  mkdirSync(path2, { recursive: true });
1432
1269
  }
1433
1270
  function copyFile(source, target) {
1434
- if (!existsSync2(source)) {
1271
+ if (!existsSync(source)) {
1435
1272
  throw new Error(`Source not found: ${source}`);
1436
1273
  }
1437
- mkdirSync(dirname(target), { recursive: true });
1274
+ mkdirSync(dirname2(target), { recursive: true });
1438
1275
  copyFileSync(source, target);
1439
1276
  }
1440
1277
  function copyDir(source, target, foreignOf) {
1441
- if (!existsSync2(source)) {
1278
+ if (!existsSync(source)) {
1442
1279
  throw new Error(`Source dir not found: ${source}`);
1443
1280
  }
1444
1281
  mkdirSync(target, { recursive: true });
@@ -1467,7 +1304,7 @@ function claimBackupPath(base, claim) {
1467
1304
  throw new Error(`Cannot allocate a backup path for ${base} (${MAX_BACKUP_CLAIMS} taken)`);
1468
1305
  }
1469
1306
  function backupDir(target, now = /* @__PURE__ */ new Date()) {
1470
- if (!existsSync2(target)) {
1307
+ if (!existsSync(target)) {
1471
1308
  return null;
1472
1309
  }
1473
1310
  const backup = claimBackupPath(`${target}.backup-${formatStamp(now)}`, (c) => mkdirSync(c));
@@ -1476,21 +1313,44 @@ function backupDir(target, now = /* @__PURE__ */ new Date()) {
1476
1313
  return backup;
1477
1314
  }
1478
1315
  function copyBackupDir(target, now = /* @__PURE__ */ new Date()) {
1479
- if (!existsSync2(target)) {
1316
+ if (!existsSync(target)) {
1480
1317
  return null;
1481
1318
  }
1482
1319
  const backup = claimBackupPath(`${target}.backup-${formatStamp(now)}`, (c) => mkdirSync(c));
1483
- cpSync(realpathSync(target), backup, { recursive: true });
1320
+ try {
1321
+ copyTreePreservingLinks(realpathSync2(target), backup);
1322
+ } catch (e) {
1323
+ rmSync(backup, { recursive: true, force: true });
1324
+ const detail = e instanceof Error ? e.message : String(e);
1325
+ throw new Error(`backup of ${target} failed, partial copy removed \u2014 ${detail}`);
1326
+ }
1484
1327
  return backup;
1485
1328
  }
1329
+ function copyTreePreservingLinks(source, target) {
1330
+ mkdirSync(target, { recursive: true });
1331
+ for (const entry of readdirSync(source, { withFileTypes: true })) {
1332
+ const src = join2(source, entry.name);
1333
+ const dst = join2(target, entry.name);
1334
+ if (entry.isSymbolicLink()) {
1335
+ symlinkSync(readlinkSync2(src), dst);
1336
+ continue;
1337
+ }
1338
+ if (entry.isDirectory()) copyTreePreservingLinks(src, dst);
1339
+ else if (entry.isFile()) copyFile(src, dst);
1340
+ }
1341
+ }
1486
1342
  function backupFile(target, now = /* @__PURE__ */ new Date()) {
1487
1343
  return claimBackupPath(
1488
1344
  `${target}.backup-${formatStamp(now)}`,
1489
1345
  (c) => copyFileSync(target, c, fsConstants.COPYFILE_EXCL)
1490
1346
  );
1491
1347
  }
1348
+ function backupOriginal(backupPath) {
1349
+ const m = /^(.+)\.backup-\d{8}T\d{6}(?:-\d+)?$/.exec(backupPath);
1350
+ return m?.[1] ?? null;
1351
+ }
1492
1352
  function listFilesRecursive(dir, prefix = "") {
1493
- if (!existsSync2(dir)) return [];
1353
+ if (!existsSync(dir)) return [];
1494
1354
  const out = [];
1495
1355
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
1496
1356
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
@@ -1515,9 +1375,93 @@ function ensureProjectSkeleton(projectDir) {
1515
1375
  "docs/decisions"
1516
1376
  ];
1517
1377
  for (const d of dirs) {
1378
+ if (outsideProjectTarget(projectDir, join2(projectDir, d)) !== null) continue;
1518
1379
  mkdirSync(join2(projectDir, d), { recursive: true });
1519
1380
  }
1520
1381
  }
1382
+ function dirTreesIdentical(a, b) {
1383
+ if (!existsSync(a) || !existsSync(b)) return false;
1384
+ const signature = (root, rel = "", out = /* @__PURE__ */ new Map()) => {
1385
+ for (const e of readdirSync(join2(root, rel), { withFileTypes: true })) {
1386
+ const r = rel === "" ? e.name : `${rel}/${e.name}`;
1387
+ if (e.isSymbolicLink()) out.set(r, `link:${readlinkSync2(join2(root, r))}`);
1388
+ else if (e.isDirectory()) signature(root, r, out);
1389
+ else if (e.isFile()) {
1390
+ out.set(
1391
+ r,
1392
+ createHash("sha256").update(readFileSync(join2(root, r))).digest("hex")
1393
+ );
1394
+ }
1395
+ }
1396
+ return out;
1397
+ };
1398
+ const sa = signature(a);
1399
+ const sb = signature(b);
1400
+ if (sa.size !== sb.size) return false;
1401
+ for (const [rel, sha] of sa) {
1402
+ if (sb.get(rel) !== sha) return false;
1403
+ }
1404
+ return true;
1405
+ }
1406
+ function backupIfLossyUtf8(target, now = /* @__PURE__ */ new Date()) {
1407
+ if (!existsSync(target)) return null;
1408
+ let raw;
1409
+ try {
1410
+ raw = readFileSync(target);
1411
+ } catch {
1412
+ return null;
1413
+ }
1414
+ if (Buffer.from(raw.toString("utf8")).equals(raw)) return null;
1415
+ const backup = backupFile(target, now);
1416
+ console.error(
1417
+ `\u26A0 ${target} contains bytes that are not valid UTF-8 \u2014 rewriting would alter them permanently. Original bytes saved as ${backup}`
1418
+ );
1419
+ return backup;
1420
+ }
1421
+
1422
+ // src/track-heal.ts
1423
+ init_esm_shims();
1424
+ import { lstatSync as lstatSync2, readFileSync as readFileSync2 } from "fs";
1425
+ import { join as join3 } from "path";
1426
+ var META_FILE = ".claude/.installed-tracks";
1427
+ var META_KEY = ".installed-tracks";
1428
+ function memoTargets(render) {
1429
+ const cache = /* @__PURE__ */ new Map();
1430
+ return (tracks) => {
1431
+ const key = [...tracks].sort().join(",");
1432
+ const hit = cache.get(key) ?? new Set(render(tracks));
1433
+ cache.set(key, hit);
1434
+ return hit;
1435
+ };
1436
+ }
1437
+ var claudeTargets = memoTargets((tracks) => {
1438
+ const spec = buildAssetSpec({ tracks, options: DEFAULT_OPTIONS });
1439
+ return buildManifest(spec).filter((e) => e.applies(spec) && (e.type === "file" || e.target.startsWith(".claude/skills/"))).map((e) => e.target);
1440
+ });
1441
+ function claudeRecorded(log) {
1442
+ return /* @__PURE__ */ new Set([
1443
+ ...(log.policyFiles ?? []).map((f) => `.claude/${f.path}`),
1444
+ ...(log.skillFiles ?? []).map((f) => `.claude/skills/${f.path.split("/")[0] ?? ""}`)
1445
+ ]);
1446
+ }
1447
+ function healRecordedTracks(log, projectDir, clis) {
1448
+ if (!clis.includes("claude")) return log;
1449
+ const recorded = log.spec.tracks.filter(isTrack);
1450
+ const added = metafileTracks(log, projectDir).filter((t) => !recorded.includes(t));
1451
+ if (added.length === 0) return log;
1452
+ return {
1453
+ ...log,
1454
+ spec: { ...log.spec, tracks: [.../* @__PURE__ */ new Set([...log.spec.tracks, ...added])].sort() }
1455
+ };
1456
+ }
1457
+ function metafileTracks(log, projectDir) {
1458
+ const abs = join3(projectDir, META_FILE);
1459
+ if (lstatSync2(abs, { throwIfNoEntry: false })?.isFile() !== true) return [];
1460
+ const content = readFileSync2(abs, "utf8");
1461
+ const sha = log.policyFiles?.find((f) => f.path === META_KEY)?.sha256;
1462
+ if (sha === void 0 || hashContent(content) !== sha) return [];
1463
+ return content.split("\n").map((l) => l.trim()).filter(isTrack);
1464
+ }
1521
1465
 
1522
1466
  // src/install-log.ts
1523
1467
  var INSTALL_LOG_FILENAME = ".harness-install.json";
@@ -1546,9 +1490,22 @@ function installedClis(log) {
1546
1490
  function sortClis(clis) {
1547
1491
  return [...new Set(clis)].filter(isCliBase).sort((a, b) => CLI_BASE_SORT_ORDER[a] - CLI_BASE_SORT_ORDER[b]);
1548
1492
  }
1549
- function buildAssetEntries(report, scope) {
1493
+ function buildAssetEntries(report, scope, previous) {
1550
1494
  if (!report) return [];
1551
- return report.attempted.filter((r) => r.ok).map((r) => assetToLogEntry(r.asset, scope, r.version));
1495
+ return report.attempted.filter((r) => r.ok).map((r) => {
1496
+ const entry = assetToLogEntry(r.asset, scope, r.version);
1497
+ if (r.files === void 0) return entry;
1498
+ const recordedBefore = previous?.find((a) => a.id === r.asset.id);
1499
+ if (recordedBefore !== void 0 && recordedBefore.files === void 0) return entry;
1500
+ return { ...entry, files: mergeToolFiles(recordedBefore?.files, r.files) };
1501
+ });
1502
+ }
1503
+ function mergeToolFiles(prior, run) {
1504
+ const changed = new Map(run.changed.map((f) => [f.path, f.sha256]));
1505
+ const byPath = /* @__PURE__ */ new Map();
1506
+ for (const f of prior ?? []) byPath.set(f.path, changed.get(f.path) ?? f.sha256);
1507
+ for (const f of run.created) byPath.set(f.path, f.sha256);
1508
+ return [...byPath].map(([path2, sha256]) => ({ path: path2, sha256 }));
1552
1509
  }
1553
1510
  function assetToLogEntry(asset, scope, version) {
1554
1511
  const detail = methodDetail(asset.method);
@@ -1577,7 +1534,7 @@ function methodDetail(method) {
1577
1534
  }
1578
1535
  }
1579
1536
  var BUNDLED_SKILL_IDS = new Set(INTERNAL_BUNDLED_SKILL_IDS);
1580
- function buildInstallLog(spec, external, scope, rootClaudeMd, previous, claudeDirMovedAside = false, rootFiles = []) {
1537
+ function buildInstallLog(spec, external, scope, rootClaudeMd, previous, claudeDirMovedAside = false, rootFiles = [], codexTrust) {
1581
1538
  const skillExclude = (spec.userOverride?.forceExclude ?? []).filter(
1582
1539
  (id) => BUNDLED_SKILL_IDS.has(id)
1583
1540
  );
@@ -1588,12 +1545,15 @@ function buildInstallLog(spec, external, scope, rootClaudeMd, previous, claudeDi
1588
1545
  ...rootClaudeMd ? { rootClaudeMd } : {}
1589
1546
  };
1590
1547
  const clis = sortClis([...installedClis(previous ?? null), ...spec.cli.filter(isCliBase)]);
1548
+ const tracks = [
1549
+ .../* @__PURE__ */ new Set([...(previous?.spec.tracks ?? []).filter(isTrack), ...spec.tracks])
1550
+ ].sort();
1591
1551
  const log = {
1592
1552
  schemaVersion: INSTALL_LOG_VERSION,
1593
1553
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1594
1554
  scope,
1595
1555
  spec: {
1596
- tracks: spec.tracks,
1556
+ tracks,
1597
1557
  cli: spec.cli,
1598
1558
  clis,
1599
1559
  ...spec.baselineExclude && spec.baselineExclude.length > 0 ? { baselineExclude: spec.baselineExclude } : {},
@@ -1604,11 +1564,13 @@ function buildInstallLog(spec, external, scope, rootClaudeMd, previous, claudeDi
1604
1564
  templates: { ...previous?.templates, ...templates },
1605
1565
  assets: mergeAssets(
1606
1566
  claudeDirMovedAside ? previous?.assets?.filter(survivesClaudeDirRename) : previous?.assets,
1607
- buildAssetEntries(external, scope)
1567
+ buildAssetEntries(external, scope, previous?.assets)
1608
1568
  )
1609
1569
  };
1610
1570
  const mergedRootFiles = mergeRootFiles(previous?.rootFiles, rootFiles);
1611
1571
  if (mergedRootFiles.length > 0) log.rootFiles = mergedRootFiles;
1572
+ const trust = codexTrust ?? previous?.codexTrust;
1573
+ if (trust) log.codexTrust = trust;
1612
1574
  return log;
1613
1575
  }
1614
1576
  function mergeRootFiles(previous, current) {
@@ -1657,35 +1619,35 @@ function mergeAssets(previous, current) {
1657
1619
  ];
1658
1620
  }
1659
1621
  function hashContent(content) {
1660
- return createHash("sha256").update(content, "utf8").digest("hex");
1622
+ return createHash2("sha256").update(content, "utf8").digest("hex");
1661
1623
  }
1662
1624
  function isHarnessOwned(baseline, key, current) {
1663
1625
  const recorded = baseline.get(key);
1664
1626
  return recorded !== void 0 && recorded === hashContent(current);
1665
1627
  }
1666
- function mergeExternalFiles(projectDir, previous, current) {
1628
+ function mergeExternalFiles(previous, current) {
1667
1629
  const byPath = /* @__PURE__ */ new Map();
1668
1630
  for (const file of [...previous ?? [], ...current]) byPath.set(file.path, file);
1669
- return [...byPath.values()].filter((f) => existsSync3(join3(projectDir, f.path)));
1631
+ return [...byPath.values()];
1670
1632
  }
1671
1633
  function collectSkillHashes(projectDir, templatesDir) {
1672
- const skillsDir = join3(projectDir, ".claude/skills");
1673
- return listFilesRecursive(skillsDir).filter((rel) => existsSync3(join3(templatesDir, "skills", rel))).map((rel) => ({
1634
+ const skillsDir = join4(projectDir, ".claude/skills");
1635
+ return listFilesRecursive(skillsDir).filter((rel) => existsSync2(join4(templatesDir, "skills", rel))).map((rel) => ({
1674
1636
  path: rel,
1675
- sha256: hashContent(readFileSync3(join3(skillsDir, rel), "utf8"))
1637
+ sha256: hashContent(readFileSync3(join4(skillsDir, rel), "utf8"))
1676
1638
  }));
1677
1639
  }
1678
1640
  function collectPolicyHashes(projectDir, templatesDir) {
1679
1641
  const out = [];
1680
1642
  for (const { dir, ext } of POLICY_DIRS) {
1681
- const targetDir = join3(projectDir, ".claude", dir);
1682
- const sourceDir = join3(templatesDir, dir);
1643
+ const targetDir = join4(projectDir, ".claude", dir);
1644
+ const sourceDir = join4(templatesDir, dir);
1683
1645
  for (const rel of listFilesRecursive(targetDir)) {
1684
1646
  if (!rel.endsWith(ext)) continue;
1685
- if (!existsSync3(join3(sourceDir, rel))) continue;
1647
+ if (!existsSync2(join4(sourceDir, rel))) continue;
1686
1648
  out.push({
1687
1649
  path: `${dir}/${rel}`,
1688
- sha256: hashContent(readFileSync3(join3(targetDir, rel), "utf8"))
1650
+ sha256: hashContent(readFileSync3(join4(targetDir, rel), "utf8"))
1689
1651
  });
1690
1652
  }
1691
1653
  }
@@ -1693,42 +1655,371 @@ function collectPolicyHashes(projectDir, templatesDir) {
1693
1655
  }
1694
1656
  function writeInstallLog(projectDir, log) {
1695
1657
  const path2 = installLogPath(projectDir);
1696
- mkdirSync2(dirname2(path2), { recursive: true });
1697
- writeFileSync(path2, `${JSON.stringify(log, null, 2)}
1658
+ mkdirSync2(dirname3(path2), { recursive: true });
1659
+ const marked = { ...normalizeLogOrder(log), excludedKeysMigrated: true };
1660
+ writeFileSync(path2, `${JSON.stringify(marked, null, 2)}
1698
1661
  `, "utf8");
1699
1662
  migrateAwayLegacyLog(projectDir);
1700
1663
  return path2;
1701
1664
  }
1665
+ function normalizeLogOrder(log) {
1666
+ const cmp = (a, b) => a < b ? -1 : a > b ? 1 : 0;
1667
+ const byPath = (xs) => [...xs].sort((a, b) => cmp(a.path, b.path));
1668
+ const out = { ...log };
1669
+ if (log.policyFiles !== void 0) out.policyFiles = byPath(log.policyFiles);
1670
+ if (log.skillFiles !== void 0) out.skillFiles = byPath(log.skillFiles);
1671
+ if (log.externalFiles !== void 0) out.externalFiles = byPath(log.externalFiles);
1672
+ if (log.rootFiles !== void 0) out.rootFiles = byPath(log.rootFiles);
1673
+ if (log.portions !== void 0) {
1674
+ out.portions = [...log.portions].sort((a, b) => cmp(a.path, b.path) || cmp(a.key, b.key));
1675
+ }
1676
+ return out;
1677
+ }
1702
1678
  function migrateAwayLegacyLog(projectDir) {
1703
1679
  const legacy = legacyInstallLogPath(projectDir);
1704
- if (!existsSync3(legacy)) return;
1680
+ if (!existsSync2(legacy)) return;
1705
1681
  try {
1706
- rmSync(legacy);
1682
+ rmSync2(legacy);
1707
1683
  } catch {
1708
1684
  }
1709
1685
  }
1710
1686
  function readInstallLog(projectDir) {
1687
+ return readInstallLogStatus(projectDir).log;
1688
+ }
1689
+ function readInstallLogStatus(projectDir) {
1711
1690
  const path2 = [installLogPath(projectDir), legacyInstallLogPath(projectDir)].find(
1712
- (p) => existsSync3(p)
1691
+ (p) => existsSync2(p)
1713
1692
  );
1714
- if (!path2) return null;
1693
+ if (!path2) return { status: "missing", log: null };
1694
+ let parsed;
1715
1695
  try {
1716
- const parsed = JSON.parse(readFileSync3(path2, "utf8"));
1717
- if (Array.isArray(parsed.assets)) {
1718
- parsed.assets = parsed.assets.map(
1719
- (a) => a.method === "npm-global" ? { ...a, method: "npm" } : a
1720
- );
1696
+ parsed = JSON.parse(readFileSync3(path2, "utf8"));
1697
+ } catch {
1698
+ return { status: "corrupted", log: null };
1699
+ }
1700
+ if (!isValidInstallLogShape(parsed)) return { status: "corrupted", log: null };
1701
+ const log = parsed;
1702
+ if (Array.isArray(log.assets)) {
1703
+ log.assets = log.assets.map(
1704
+ (a) => a.method === "npm-global" ? { ...a, method: "npm" } : a
1705
+ );
1706
+ }
1707
+ const migrated = migrateExcluded(log, projectDir);
1708
+ if (migrated.droppedKeys.length > 0) {
1709
+ LEGACY_RELEASED.set(migrated.log, {
1710
+ released: migrated.droppedKeys,
1711
+ catalog: migrated.releasedCatalog
1712
+ });
1713
+ }
1714
+ return {
1715
+ status: "ok",
1716
+ log: healRecordedTracks(migrated.log, projectDir, installedClis(migrated.log))
1717
+ };
1718
+ }
1719
+ var LEGACY_RELEASED = /* @__PURE__ */ new WeakMap();
1720
+ function legacyDroppedKeys(log) {
1721
+ return log === null ? [] : LEGACY_RELEASED.get(log)?.released ?? [];
1722
+ }
1723
+ function legacyReleasedCatalog(log) {
1724
+ return log === null ? [] : LEGACY_RELEASED.get(log)?.catalog ?? [];
1725
+ }
1726
+ var SELECTIONS_MAX = 100;
1727
+ var HARNESS_VERSION = package_default.version;
1728
+ function appendSelection(log, ev) {
1729
+ const empty = (a) => a === void 0 || a.length === 0;
1730
+ if (empty(ev.with) && empty(ev.without) && empty(ev.released) && empty(ev.kept)) return log;
1731
+ const clean = {
1732
+ at: ev.at,
1733
+ harness: ev.harness,
1734
+ by: ev.by,
1735
+ ...ev.via !== void 0 ? { via: ev.via } : {},
1736
+ ...empty(ev.with) ? {} : { with: [...ev.with ?? []] },
1737
+ ...empty(ev.without) ? {} : { without: [...ev.without ?? []] },
1738
+ ...empty(ev.released) ? {} : { released: [...ev.released ?? []] },
1739
+ ...empty(ev.kept) ? {} : { kept: [...ev.kept ?? []] },
1740
+ ...ev.interrupted ? { interrupted: true } : {}
1741
+ };
1742
+ return { ...log, selections: [...log.selections ?? [], clean].slice(-SELECTIONS_MAX) };
1743
+ }
1744
+ var LEGACY_REINSTALL_WINDOW_MS = 6e5;
1745
+ function assetFiles(projectDir, asset) {
1746
+ if (asset.files !== void 0) {
1747
+ return asset.files.map((f) => join4(projectDir, f.path)).filter((p) => existsSync2(p));
1748
+ }
1749
+ const dir = asset.detail.skill ?? asset.id;
1750
+ return [".claude/skills", ".agents/skills"].flatMap(
1751
+ (d) => listFilesRecursive(join4(projectDir, d, dir)).map((rel) => join4(projectDir, d, dir, rel))
1752
+ );
1753
+ }
1754
+ function legacyCatalogReleased(projectDir, log, asset) {
1755
+ if (asset.method !== "skill" || asset.scope === "global") return false;
1756
+ const files = assetFiles(projectDir, asset);
1757
+ if (files.length === 0) return false;
1758
+ const t = Date.parse(log.installedAt);
1759
+ if (Number.isNaN(t)) return false;
1760
+ const m = Math.max(...files.map((f) => statSync(f).mtimeMs));
1761
+ return m <= t && m >= t - LEGACY_REINSTALL_WINDOW_MS;
1762
+ }
1763
+ function migrateExcluded(log, projectDir) {
1764
+ if (log.excludedKeysMigrated === true) return { log, droppedKeys: [], releasedCatalog: [] };
1765
+ const baseline = new Set(log.spec.baselineExclude ?? []);
1766
+ const skills = new Set(log.spec.skillExclude ?? []);
1767
+ const released = [];
1768
+ const releasedCatalog = [];
1769
+ const keptCatalog = [];
1770
+ const kept = [];
1771
+ for (const id of log.excluded ?? []) {
1772
+ if (isKeyId(id)) {
1773
+ released.push(id);
1774
+ continue;
1775
+ }
1776
+ if (id.startsWith("baseline:") && !baseline.has(id)) {
1777
+ released.push(id);
1778
+ continue;
1779
+ }
1780
+ if (BUNDLED_SKILL_IDS.has(id) && !skills.has(id)) {
1781
+ released.push(id);
1782
+ continue;
1783
+ }
1784
+ const asset = log.assets.find((a) => a.id === id);
1785
+ if (asset !== void 0 && !BUNDLED_SKILL_IDS.has(id) && !id.startsWith("baseline:")) {
1786
+ if (projectDir !== void 0 && legacyCatalogReleased(projectDir, log, asset)) {
1787
+ released.push(id);
1788
+ releasedCatalog.push(id);
1789
+ continue;
1790
+ }
1791
+ keptCatalog.push(id);
1792
+ }
1793
+ kept.push(id);
1794
+ }
1795
+ let next = { ...log, excludedKeysMigrated: true };
1796
+ if (kept.length > 0) next.excluded = kept;
1797
+ else delete next.excluded;
1798
+ next = appendSelection(next, {
1799
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1800
+ harness: HARNESS_VERSION,
1801
+ by: "migration",
1802
+ released,
1803
+ kept: keptCatalog
1804
+ });
1805
+ return { log: next, droppedKeys: released, releasedCatalog };
1806
+ }
1807
+ function isValidInstallLogShape(value) {
1808
+ if (typeof value !== "object" || value === null) return false;
1809
+ const v = value;
1810
+ const spec = v.spec;
1811
+ if (typeof spec !== "object" || spec === null) return false;
1812
+ if (!Array.isArray(spec.tracks)) return false;
1813
+ if (!Array.isArray(v.assets)) return false;
1814
+ if (typeof v.templates !== "object" || v.templates === null) return false;
1815
+ return true;
1816
+ }
1817
+ function corruptedInstallLogMessage(projectDir) {
1818
+ return `install log is corrupted at ${installLogPath(projectDir)} \u2014 if it holds git conflict markers, take one side whole; otherwise run install --reinstall (it forgets recorded exclusions and external assets)`;
1819
+ }
1820
+ function installLogPath(projectDir) {
1821
+ return join4(projectDir, INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
1822
+ }
1823
+ function legacyInstallLogPath(projectDir) {
1824
+ return join4(projectDir, LEGACY_INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
1825
+ }
1826
+
1827
+ // src/project-claude-merge.ts
1828
+ init_esm_shims();
1829
+ var HARNESS_ANCHOR_FILE = "CLAUDE-uzys-harness.md";
1830
+ var HARNESS_IMPORT_LINE = `@${HARNESS_ANCHOR_FILE}`;
1831
+ var IMPORT_MARKER_START = "<!-- uzys-harness:import:start -->";
1832
+ var IMPORT_MARKER_END = "<!-- uzys-harness:import:end -->";
1833
+ var CONTINUOUS_SKILLS_HEADING = "## Skills that apply continuously";
1834
+ function renderContinuousSkillsNote(selectedInternalSkills) {
1835
+ const lines = CONTINUOUS_SKILLS.filter((s) => selectedInternalSkills.includes(s.id)).map(
1836
+ (s) => `- \`${s.id}\` \u2014 ${s.whenToApply}.`
1837
+ );
1838
+ if (lines.length === 0) return "";
1839
+ return [
1840
+ CONTINUOUS_SKILLS_HEADING,
1841
+ "",
1842
+ "These installed skills apply to every response or delegation; no prompt ever looks like their job, so open them from this list:",
1843
+ "",
1844
+ ...lines
1845
+ ].join("\n");
1846
+ }
1847
+ function withContinuousSkillsNote(scaffold, selectedInternalSkills) {
1848
+ const note = renderContinuousSkillsNote(selectedInternalSkills);
1849
+ return note ? `${scaffold}
1850
+
1851
+ ${note}` : scaffold;
1852
+ }
1853
+ function importBlock(continuousSkills) {
1854
+ const note = renderContinuousSkillsNote(continuousSkills);
1855
+ const body = note ? `${HARNESS_IMPORT_LINE}
1856
+
1857
+ ${note}` : HARNESS_IMPORT_LINE;
1858
+ return `${IMPORT_MARKER_START}
1859
+ ${body}
1860
+ ${IMPORT_MARKER_END}`;
1861
+ }
1862
+ var TRACK_DISPLAY_NAMES = {
1863
+ base: "Base",
1864
+ tooling: "Tooling",
1865
+ "csr-supabase": "CSR Supabase",
1866
+ "csr-fastify": "CSR Fastify",
1867
+ "csr-fastapi": "CSR FastAPI",
1868
+ "ssr-htmx": "SSR HTMX",
1869
+ "ssr-nextjs": "SSR Next.js",
1870
+ data: "Data",
1871
+ executive: "Executive",
1872
+ full: "Full",
1873
+ "project-management": "Project Management",
1874
+ "growth-marketing": "Growth Marketing"
1875
+ };
1876
+ var FULL_EXPANSION = TRACKS.filter((t) => t !== "full");
1877
+ var FILL_SECTIONS = [
1878
+ "identity",
1879
+ "stack",
1880
+ "architecture",
1881
+ "installed-assets",
1882
+ "boundaries",
1883
+ "verify"
1884
+ ];
1885
+ var FILL_SPECS = {
1886
+ identity: {
1887
+ title: "Identity & Purpose",
1888
+ prompt: `Replace the H1 title above with this project's real name, then state in 1-2 plain sentences what it does, who uses it, and why it exists. Sources: README.md, the package.json / pyproject.toml "description", docs/. Do NOT describe the harness itself. Delete this comment when done.`,
1889
+ placeholder: "what this project is, who it is for, and why it exists"
1890
+ },
1891
+ stack: {
1892
+ title: "Stack & Commands",
1893
+ prompt: "Replace the list below with this project's REAL stack. Inspect package.json / pyproject.toml / go.mod / Cargo.toml / Gemfile + lockfiles and any Makefile/justfile/package scripts. List the language(s)+versions, framework(s), package manager, and the exact install/build/test/run commands. Verify each command exists before writing it \u2014 never guess. Delete lines that do not apply, then delete this comment.",
1894
+ placeholder: "languages, runtimes, package manager, and the install/build/test/run commands"
1895
+ },
1896
+ architecture: {
1897
+ title: "Architecture & Layout",
1898
+ prompt: "Map this repository. List each top-level source directory and what it holds, the entry point(s), the 3-5 files a newcomer reads first, and how data/requests flow between the main layers. View the real tree (e.g. `git ls-files | sed 's#/.*##' | sort -u`) \u2014 do not assume a framework's conventional layout. Flag any generated/vendored directories that must never be hand-edited. If this is a single small module, say so in one line. Delete this comment when done.",
1899
+ placeholder: "where things live, the entry points, and how data flows between layers"
1900
+ },
1901
+ "installed-assets": {
1902
+ title: "Installed Harness Assets",
1903
+ prompt: "List the harness assets installed in this project (rules / skills / agents / commands) and add one line each on when to reach for it here. Verify against .claude/rules/*.md, .claude/skills/, .claude/agents/, .claude/commands/ (or this CLI's equivalent) and list ONLY assets whose file exists on disk \u2014 do not invent any. Do NOT restate the harness's working principles (they live in this project's harness anchor and rules layer) \u2014 cross-reference them instead. Delete this comment when done.",
1904
+ placeholder: "the installed rules/skills/agents/commands and when to use each"
1905
+ },
1906
+ boundaries: {
1907
+ title: "Boundaries \u2014 Always / Ask First / Never",
1908
+ prompt: `Fill the Always / Ask First / Never lists with this repo's real red lines. Read the CI config, CODEOWNERS, .gitignore, and release/deploy scripts. Never must include secrets, generated files, force-push, and direct commits to the default branch, plus any repo-specific "do not touch" directories. Every entry must be specific and enforceable here. Delete this comment when done.`,
1909
+ placeholder: "what to always do, ask before doing, and never do in this repo"
1910
+ },
1911
+ verify: {
1912
+ title: "Verification Gate",
1913
+ prompt: 'State the single command (or short sequence) that PROVES a change is safe here \u2014 the test/lint/typecheck/build gate you run before committing \u2014 plus the coverage/CI threshold and what "done" means. Sources: package scripts, CI workflow files, CONTRIBUTING. An agent must be able to self-verify without guessing. Delete this comment when done.',
1914
+ placeholder: "the single command that proves a change is safe, and the done bar"
1915
+ }
1916
+ };
1917
+ function anchorLine(surface) {
1918
+ if (surface === "claude") {
1919
+ return `> The working principles live in this project's harness anchor \`${HARNESS_ANCHOR_FILE}\`, imported at the bottom of this file. Everything above that import is **project-specific context only**.`;
1920
+ }
1921
+ const self = surface === "antigravity-rule" ? "`.agents/rules/uzys-harness.md`" : "`AGENTS.md`";
1922
+ return `> **This file (${self}) is the harness anchor** \u2014 it carries the working principles in full, further down. Only the \`## \u2026\` sections immediately below are project-specific context for you to fill in.`;
1923
+ }
1924
+ function scaffoldBanner(surface) {
1925
+ return [
1926
+ "> \u2699\uFE0F **SCAFFOLD \u2014 not filled in yet.** The sections below are a fill-in template for THIS project, not verified facts.",
1927
+ "> To fill: open this file and paste each `<!-- FILL: \u2026 -->` comment's instruction into your coding agent (e.g. Claude Code) \u2014 it will inspect the real repo and write the section. You can also fill them by hand; the comments are the instructions.",
1928
+ anchorLine(surface)
1929
+ ].join("\n");
1930
+ }
1931
+ function renderFillScaffold(surface = "claude") {
1932
+ const blocks = FILL_SECTIONS.map((id) => {
1933
+ const spec = FILL_SPECS[id];
1934
+ return `## ${spec.title}
1935
+
1936
+ <!-- FILL:${id} \u2014 ${spec.prompt} -->
1937
+
1938
+ _(not filled yet \u2014 ${spec.placeholder})_`;
1939
+ });
1940
+ return `${scaffoldBanner(surface)}
1941
+
1942
+ ${blocks.join("\n\n")}`;
1943
+ }
1944
+ function mergeProjectClaude(opts) {
1945
+ const expanded = expandTracks(opts.tracks);
1946
+ const trackList = expanded.map((t) => TRACK_DISPLAY_NAMES[t]).join(", ");
1947
+ const header = `# ${opts.projectName}
1948
+
1949
+ > Active track(s): ${trackList}`;
1950
+ return `${header}
1951
+
1952
+ ${opts.projectContext ?? renderFillScaffold()}
1953
+ `;
1954
+ }
1955
+ function upsertHarnessImport(existing, opts) {
1956
+ const block = importBlock(opts.continuousSkills ?? []);
1957
+ if (existing === null) {
1958
+ return `${mergeProjectClaude(opts)}
1959
+ ${block}
1960
+ `;
1961
+ }
1962
+ const start = existing.indexOf(IMPORT_MARKER_START);
1963
+ if (start !== -1) {
1964
+ const endAt = existing.indexOf(IMPORT_MARKER_END, start);
1965
+ if (endAt === -1) return existing;
1966
+ const current = existing.slice(start, endAt + IMPORT_MARKER_END.length);
1967
+ if (current === block) return existing;
1968
+ return `${existing.slice(0, start)}${block}${existing.slice(endAt + IMPORT_MARKER_END.length)}`;
1969
+ }
1970
+ if (hasHarnessImport(existing)) {
1971
+ return existing;
1972
+ }
1973
+ const body = existing.endsWith("\n") ? existing : `${existing}
1974
+ `;
1975
+ return `${body}
1976
+ ${block}
1977
+ `;
1978
+ }
1979
+ function stripHarnessImport(text) {
1980
+ if (!text.includes(IMPORT_MARKER_START) && !text.includes(IMPORT_MARKER_END)) return null;
1981
+ const endsWithNewline = text.endsWith("\n");
1982
+ const out = [];
1983
+ const lines = text.split("\n");
1984
+ let i = 0;
1985
+ while (i < lines.length) {
1986
+ const line = lines[i] ?? "";
1987
+ if (line.includes(IMPORT_MARKER_START)) {
1988
+ let j = i;
1989
+ while (j < lines.length && !(lines[j] ?? "").includes(IMPORT_MARKER_END)) j++;
1990
+ i = j < lines.length ? j + 1 : i + 1;
1991
+ if (out.length > 0 && out[out.length - 1] === "") out.pop();
1992
+ continue;
1993
+ }
1994
+ if (line.includes(IMPORT_MARKER_END) || line.trim() === HARNESS_IMPORT_LINE) {
1995
+ i++;
1996
+ continue;
1997
+ }
1998
+ out.push(line);
1999
+ i++;
2000
+ }
2001
+ let result = out.join("\n");
2002
+ if (endsWithNewline && result !== "" && !result.endsWith("\n")) result += "\n";
2003
+ if (!endsWithNewline && result.endsWith("\n")) result = result.slice(0, -1);
2004
+ return result === text ? null : result;
2005
+ }
2006
+ function hasHarnessImport(text) {
2007
+ if (text.includes(IMPORT_MARKER_START)) return true;
2008
+ let inFence = false;
2009
+ for (const line of text.split("\n")) {
2010
+ if (/^\s*(?:```|~~~)/.test(line)) {
2011
+ inFence = !inFence;
2012
+ continue;
1721
2013
  }
1722
- return parsed;
1723
- } catch {
1724
- return null;
2014
+ if (!inFence && line.trim() === HARNESS_IMPORT_LINE) return true;
1725
2015
  }
2016
+ return false;
1726
2017
  }
1727
- function installLogPath(projectDir) {
1728
- return join3(projectDir, INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
1729
- }
1730
- function legacyInstallLogPath(projectDir) {
1731
- return join3(projectDir, LEGACY_INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
2018
+ function expandTracks(tracks) {
2019
+ if (tracks.includes("full")) {
2020
+ return FULL_EXPANSION;
2021
+ }
2022
+ return tracks;
1732
2023
  }
1733
2024
 
1734
2025
  // src/manifest.ts
@@ -1743,7 +2034,6 @@ function buildAssetSpec(ctx) {
1743
2034
  }
1744
2035
  var all = () => true;
1745
2036
  var dev = (s) => hasDevTrack(s.tracks);
1746
- var ui = (s) => hasUiTrack(s.tracks);
1747
2037
  var onTracks = (pattern) => (s) => anyTrack(s.tracks, pattern);
1748
2038
  var COMMON_RULES = ["git-policy", "change-management", "doc-governance"];
1749
2039
  var DEV_RULES = ["test-policy", "ship-checklist"];
@@ -1856,7 +2146,6 @@ var RETIRED_AGENT_IDS = RETIRED_PATHS.filter(
1856
2146
  var ALWAYS_HOOKS = ["session-start.sh", "protect-files.sh"];
1857
2147
  var COMMON_SKILL_DIRS = [];
1858
2148
  var DEV_SKILL_DIRS = [];
1859
- var UI_SKILL_DIRS = ["ui-visual-review"];
1860
2149
  function isCliNeutralTarget(target) {
1861
2150
  return target.startsWith(`${INSTALL_LOG_DIR}/`);
1862
2151
  }
@@ -1931,7 +2220,14 @@ function buildManifest(spec) {
1931
2220
  source: `skills/${sd}`,
1932
2221
  target: `.claude/skills/${sd}`,
1933
2222
  type: "dir",
1934
- applies: (s) => (s.selectedInternalSkills ?? []).includes(sd)
2223
+ // #602 — selectedInternalSkills 가 SSOT(위저드 해제·--without 반영). `tracks` 만 담긴
2224
+ // 스펙(listBaselineTargets·위저드 1단계 등)에서는 카탈로그 조건으로 판정한다 —
2225
+ // buildAssetSpec 와 같은 isAssetSelected 라 두 번 적는 사본이 아니라 같은 원천이다.
2226
+ applies: (s) => s.selectedInternalSkills !== void 0 ? s.selectedInternalSkills.includes(sd) : (
2227
+ // #602 폴백은 트랙 추종 번들 스킬 한정 — 카탈로그 옵트인(compaction-handoff 등)은
2228
+ // 선택 목록 없이는 깔리지 않는다(위저드 해제·--without 의 SSOT 를 우회하는 금이 된다).
2229
+ sd === "ui-visual-review" && isAssetSelected(sd, { tracks: s.tracks, options: { withCodexTrust: false } })
2230
+ )
1935
2231
  });
1936
2232
  }
1937
2233
  for (const sd of DEV_SKILL_DIRS) {
@@ -1942,14 +2238,6 @@ function buildManifest(spec) {
1942
2238
  applies: dev
1943
2239
  });
1944
2240
  }
1945
- for (const sd of UI_SKILL_DIRS) {
1946
- m.push({
1947
- source: `skills/${sd}`,
1948
- target: `.claude/skills/${sd}`,
1949
- type: "dir",
1950
- applies: ui
1951
- });
1952
- }
1953
2241
  for (const h of ALWAYS_HOOKS) {
1954
2242
  m.push({
1955
2243
  source: `hooks/${h}`,
@@ -1967,6 +2255,212 @@ function buildManifest(spec) {
1967
2255
  return m;
1968
2256
  }
1969
2257
 
2258
+ // src/context-cost.ts
2259
+ init_esm_shims();
2260
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
2261
+ import { join as join5, resolve as resolve2 } from "path";
2262
+ import { fileURLToPath as fileURLToPath2 } from "url";
2263
+ var CHARS_PER_TOKEN = 4;
2264
+ function estimateTokens(chars) {
2265
+ return Math.ceil(chars / CHARS_PER_TOKEN);
2266
+ }
2267
+ function resolveBundleRoot() {
2268
+ return resolve2(fileURLToPath2(new URL(".", import.meta.url)), "..");
2269
+ }
2270
+ function extractFrontmatter(content) {
2271
+ const m = content.match(/^?---\r?\n([\s\S]*?)\r?\n---/);
2272
+ return m?.[1] ?? null;
2273
+ }
2274
+ function assetDescriptorTokens(assetId, root = resolveBundleRoot()) {
2275
+ const asset = EXTERNAL_ASSETS.find((a) => a.id === assetId);
2276
+ if (!asset || asset.method.kind !== "internal") return null;
2277
+ const skillMd = join5(root, "templates", "skills", asset.method.key, "SKILL.md");
2278
+ if (!existsSync3(skillMd)) return null;
2279
+ const fm = extractFrontmatter(readFileSync4(skillMd, "utf8"));
2280
+ return fm === null ? null : estimateTokens(fm.length);
2281
+ }
2282
+ function assetBodyTokens(assetId, root = resolveBundleRoot()) {
2283
+ const asset = EXTERNAL_ASSETS.find((a) => a.id === assetId);
2284
+ if (!asset || asset.method.kind !== "internal") return null;
2285
+ const skillMd = join5(root, "templates", "skills", asset.method.key, "SKILL.md");
2286
+ if (!existsSync3(skillMd)) return null;
2287
+ const content = readFileSync4(skillMd, "utf8");
2288
+ const fm = extractFrontmatter(content);
2289
+ if (fm === null) return estimateTokens(content.length);
2290
+ const close = content.indexOf("\n---", content.indexOf(fm) + fm.length);
2291
+ const body = close === -1 ? "" : content.slice(close + "\n---".length);
2292
+ return estimateTokens(body.trim().length);
2293
+ }
2294
+ function assetCostRows(assetIds, root = resolveBundleRoot()) {
2295
+ return assetIds.map((id) => ({
2296
+ id,
2297
+ descriptorTokens: assetDescriptorTokens(id, root),
2298
+ bodyTokens: assetBodyTokens(id, root)
2299
+ })).sort((a, b) => (b.bodyTokens ?? -1) - (a.bodyTokens ?? -1));
2300
+ }
2301
+ function makeResidentCost(base) {
2302
+ return {
2303
+ ...base,
2304
+ contextFile: base.contextFile ?? "CLAUDE.md",
2305
+ perSkillDescriptor: base.perSkillDescriptor ?? {},
2306
+ total: base.rules + base.projectClaudeMd + base.skillDescriptors + base.agentDescriptors,
2307
+ directive: {
2308
+ items: base.items.rules + base.items.claudeMd,
2309
+ tokens: base.rules + base.projectClaudeMd
2310
+ },
2311
+ firing: {
2312
+ items: base.items.skills + base.items.agents,
2313
+ tokens: base.skillDescriptors + base.agentDescriptors
2314
+ }
2315
+ };
2316
+ }
2317
+ function fileTokens(path2) {
2318
+ return existsSync3(path2) ? estimateTokens(readFileSync4(path2, "utf8").trim().length) : 0;
2319
+ }
2320
+ function descriptorTokens(path2) {
2321
+ if (!existsSync3(path2)) return 0;
2322
+ const fm = extractFrontmatter(readFileSync4(path2, "utf8"));
2323
+ return fm === null ? 0 : estimateTokens(fm.length);
2324
+ }
2325
+ function anchorSurface(cli) {
2326
+ if (cli.includes("claude") || cli.length === 0) {
2327
+ return { label: "CLAUDE.md", files: 2, scaffold: "claude" };
2328
+ }
2329
+ if (cli.includes("codex") || cli.includes("opencode")) {
2330
+ return { label: "AGENTS.md", files: 1, scaffold: "agents-md" };
2331
+ }
2332
+ return { label: ".agents/rules/uzys-harness.md", files: 1, scaffold: "antigravity-rule" };
2333
+ }
2334
+ function residentCost(entries, root = resolveBundleRoot(), cli = ["claude"], contextFiles) {
2335
+ const tpl = (source) => join5(root, "templates", source);
2336
+ const measured = (e) => e.file ?? tpl(e.source);
2337
+ const perSkill = {};
2338
+ let rules = 0;
2339
+ let skillDescriptors = 0;
2340
+ let agentDescriptors = 0;
2341
+ let ruleItems = 0;
2342
+ let skillItems = 0;
2343
+ let agentItems = 0;
2344
+ for (const e of entries) {
2345
+ if (e.target.startsWith(".claude/rules/")) {
2346
+ rules += fileTokens(measured(e));
2347
+ ruleItems += 1;
2348
+ } else if (e.target.startsWith(".claude/agents/")) {
2349
+ agentDescriptors += descriptorTokens(measured(e));
2350
+ agentItems += 1;
2351
+ } else if (e.target.startsWith(".claude/skills/")) {
2352
+ const t = descriptorTokens(join5(measured(e), "SKILL.md"));
2353
+ skillDescriptors += t;
2354
+ skillItems += 1;
2355
+ const id = /\.claude\/skills\/([^/]+)/.exec(e.target)?.[1];
2356
+ if (id !== void 0) perSkill[id] = (perSkill[id] ?? 0) + t;
2357
+ }
2358
+ }
2359
+ const harnessAnchor = fileTokens(join5(root, "templates", "CLAUDE.md"));
2360
+ const surface = anchorSurface(cli);
2361
+ const projectScaffold = estimateTokens(
2362
+ withContinuousSkillsNote(
2363
+ renderFillScaffold(surface.scaffold),
2364
+ CONTINUOUS_SKILLS.map((s) => s.id)
2365
+ ).trim().length
2366
+ );
2367
+ const onDisk = contextFiles?.map(fileTokens).filter((t) => t > 0);
2368
+ const projectClaudeMd = onDisk === void 0 ? harnessAnchor + projectScaffold : onDisk.reduce((a, b) => a + b, 0);
2369
+ const claudeMdItems = onDisk !== void 0 ? onDisk.length : surface.files === 1 ? 1 : (harnessAnchor > 0 ? 1 : 0) + (projectScaffold > 0 ? 1 : 0);
2370
+ return makeResidentCost({
2371
+ rules,
2372
+ projectClaudeMd,
2373
+ skillDescriptors,
2374
+ agentDescriptors,
2375
+ perSkillDescriptor: perSkill,
2376
+ contextFile: surface.label,
2377
+ items: {
2378
+ rules: ruleItems,
2379
+ skills: skillItems,
2380
+ agents: agentItems,
2381
+ claudeMd: claudeMdItems,
2382
+ total: ruleItems + skillItems + agentItems + claudeMdItems
2383
+ }
2384
+ });
2385
+ }
2386
+ function summarizeContextCost(assetIds, root = resolveBundleRoot()) {
2387
+ let measuredTokens = 0;
2388
+ let measuredCount = 0;
2389
+ let unmeasuredCount = 0;
2390
+ for (const id of assetIds) {
2391
+ const tokens = assetDescriptorTokens(id, root);
2392
+ if (tokens === null) unmeasuredCount += 1;
2393
+ else {
2394
+ measuredTokens += tokens;
2395
+ measuredCount += 1;
2396
+ }
2397
+ }
2398
+ return { measuredTokens, measuredCount, unmeasuredCount };
2399
+ }
2400
+ function formatResidentCostLine(r, unmeasuredCount) {
2401
+ if (r.total === 0) return null;
2402
+ const parts = [
2403
+ `directives ${r.directive.items} ~${r.directive.tokens}`,
2404
+ `[rules ${r.items.rules} ~${r.rules} \xB7 ${r.contextFile} ${r.items.claudeMd} ~${r.projectClaudeMd}]`,
2405
+ `triggers ${r.firing.items} ~${r.firing.tokens}`,
2406
+ `[skills ${r.items.skills} ~${r.skillDescriptors} \xB7 agents ${r.items.agents} ~${r.agentDescriptors}]`
2407
+ ].join(" \xB7 ");
2408
+ const external = unmeasuredCount > 0 ? ` \xB7 ${unmeasuredCount} external asset${unmeasuredCount === 1 ? "" : "s"} unmeasured` : "";
2409
+ return `session-start context cost: ${r.items.total} items resident \xB7 ~${r.total} tokens/session (${parts}${external})`;
2410
+ }
2411
+ var RESIDENT_ROWS = [
2412
+ {
2413
+ // ADR-083 — 축 소계를 **행으로** 낸다. 소계가 없으면 읽는 사람이 네 줄을 머리로 더해야
2414
+ // 하고, 그러면 "무엇을 줄여야 하나"가 표에서 안 보인다.
2415
+ label: "\uC9C0\uC2DC\uBB38",
2416
+ gap: 13,
2417
+ count: (r) => r.directive.items,
2418
+ tokens: (r) => r.directive.tokens
2419
+ },
2420
+ { label: " rules", gap: 12, count: (r) => r.items.rules, tokens: (r) => r.rules },
2421
+ {
2422
+ // 한 행에 두 파일(앵커 + 스캐폴드). 개수 열이 2 를 뱉어 그 사실을 드러낸다 — 라벨로
2423
+ // 하나만 이름 붙이면 v26.140.0 이전과 같은 거짓 라벨이 된다.
2424
+ label: " CLAUDE.md",
2425
+ gap: 8,
2426
+ count: (r) => r.items.claudeMd,
2427
+ tokens: (r) => r.projectClaudeMd
2428
+ },
2429
+ {
2430
+ label: "\uBC1C\uD654 \uD45C\uBA74",
2431
+ gap: 10,
2432
+ count: (r) => r.firing.items,
2433
+ tokens: (r) => r.firing.tokens
2434
+ },
2435
+ {
2436
+ label: " skill descriptors",
2437
+ gap: 0,
2438
+ count: (r) => r.items.skills,
2439
+ tokens: (r) => r.skillDescriptors
2440
+ },
2441
+ {
2442
+ label: " agent descriptors",
2443
+ gap: 0,
2444
+ count: (r) => r.items.agents,
2445
+ tokens: (r) => r.agentDescriptors
2446
+ }
2447
+ ];
2448
+ function formatResidentCostBlock(r) {
2449
+ const n = (v) => String(v).padStart(3);
2450
+ return [
2451
+ " \uAC1C\uC218 \uD1A0\uD070",
2452
+ ...RESIDENT_ROWS.map(
2453
+ (row) => ` ${row.label}${" ".repeat(row.gap)}${n(row.count(r))}\uAC1C ~${row.tokens(r)}`
2454
+ ),
2455
+ " \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
2456
+ ` \uC0C1\uC8FC \uD569\uACC4 ${n(r.items.total)}\uAC1C \uC0C1\uC8FC \xB7 ~${r.total} tokens/\uC138\uC158`,
2457
+ "",
2458
+ " \uC9C0\uC2DC\uBB38 = \uC6B0\uB9AC\uAC00 \uD56D\uC0C1 \uC77D\uD788\uAC8C \uB123\uC740 \uAC83. **\uC791\uC744\uC218\uB85D \uC88B\uC740 \uAC12\uC774 \uC544\uB2C8\uB2E4** \u2014 \uD310\uB2E8\uC740",
2459
+ " '\uBD88\uD544\uC694\uD55C \uC9C0\uC2DC\uBB38\uC774 \uC788\uB098'\uB85C \uD55C\uB2E4. ratchet \uC740 \uC870\uC6A9\uD55C \uC99D\uAC00\uB9CC \uB9C9\uB294\uB2E4.",
2460
+ " \uBC1C\uD654 \uD45C\uBA74 = \uC2A4\uD0AC\xB7\uC5D0\uC774\uC804\uD2B8\uAC00 \uBD88\uB9AC\uB824\uACE0 \uC0C1\uC8FC\uC2DC\uD0A4\uB294 descriptor. \uAE4E\uC73C\uBA74 \uC548 \uBD88\uB9B0\uB2E4."
2461
+ ];
2462
+ }
2463
+
1970
2464
  // src/categories.ts
1971
2465
  init_esm_shims();
1972
2466
  var CATEGORIES = [
@@ -1996,8 +2490,10 @@ export {
1996
2490
  __commonJS,
1997
2491
  __toESM,
1998
2492
  init_esm_shims,
1999
- anyTrack,
2000
- hasUiTrack,
2493
+ package_default,
2494
+ SHARED_FILES,
2495
+ isKeyId,
2496
+ sharedPathOfKeyId,
2001
2497
  TRACKS,
2002
2498
  isTrack,
2003
2499
  CLI_BASES,
@@ -2006,6 +2502,11 @@ export {
2006
2502
  resolveScope,
2007
2503
  DEFAULT_OPTIONS,
2008
2504
  UPDATE_GROUPS,
2505
+ CLI_BASE_SORT_ORDER,
2506
+ parseCliTargets,
2507
+ targetsInclude,
2508
+ anyTrack,
2509
+ hasUiTrack,
2009
2510
  EXTERNAL_ASSETS,
2010
2511
  DEV_METHOD_SKILL_IDS,
2011
2512
  INTERNAL_BUNDLED_SKILL_IDS,
@@ -2019,6 +2520,22 @@ export {
2019
2520
  assetCliSupport,
2020
2521
  assetReachesCli,
2021
2522
  filterApplicableAssets,
2523
+ isOutsideProject,
2524
+ outsideProjectTarget,
2525
+ outsideLinkAt,
2526
+ createOutsideGuard,
2527
+ mergeOutside,
2528
+ ensureDir,
2529
+ copyFile,
2530
+ copyDir,
2531
+ backupDir,
2532
+ copyBackupDir,
2533
+ backupFile,
2534
+ backupOriginal,
2535
+ listFilesRecursive,
2536
+ ensureProjectSkeleton,
2537
+ dirTreesIdentical,
2538
+ backupIfLossyUtf8,
2022
2539
  HARNESS_ANCHOR_FILE,
2023
2540
  HARNESS_IMPORT_LINE,
2024
2541
  CONTINUOUS_SKILLS_HEADING,
@@ -2028,31 +2545,24 @@ export {
2028
2545
  renderFillScaffold,
2029
2546
  upsertHarnessImport,
2030
2547
  stripHarnessImport,
2031
- estimateTokens,
2032
- resolveBundleRoot,
2033
- extractFrontmatter,
2034
- assetCostRows,
2035
- landsOnDisk,
2036
- residentCost,
2037
- summarizeContextCost,
2038
- formatResidentCostLine,
2039
- formatResidentCostBlock,
2040
- CLI_BASE_SORT_ORDER,
2041
- parseCliTargets,
2042
- targetsInclude,
2043
- ensureDir,
2044
- copyFile,
2045
- copyDir,
2046
- backupDir,
2047
- copyBackupDir,
2048
- backupFile,
2049
- listFilesRecursive,
2050
- ensureProjectSkeleton,
2548
+ buildAssetSpec,
2549
+ resolveRules,
2550
+ TRACK_AGENTS,
2551
+ RETIRED_AGENTS,
2552
+ RETIRED_PATHS,
2553
+ RETIRED_AGENT_IDS,
2554
+ isCliNeutralTarget,
2555
+ buildManifest,
2556
+ memoTargets,
2557
+ claudeTargets,
2558
+ claudeRecorded,
2051
2559
  INSTALL_LOG_FILENAME,
2052
2560
  INSTALL_LOG_DIR,
2561
+ LEGACY_INSTALL_LOG_DIR,
2053
2562
  POLICY_DIRS,
2054
2563
  installedClis,
2055
2564
  buildInstallLog,
2565
+ mergeRootFiles,
2056
2566
  hashContent,
2057
2567
  isHarnessOwned,
2058
2568
  mergeExternalFiles,
@@ -2060,18 +2570,23 @@ export {
2060
2570
  collectPolicyHashes,
2061
2571
  writeInstallLog,
2062
2572
  readInstallLog,
2573
+ readInstallLogStatus,
2574
+ legacyDroppedKeys,
2575
+ legacyReleasedCatalog,
2576
+ HARNESS_VERSION,
2577
+ appendSelection,
2578
+ corruptedInstallLogMessage,
2063
2579
  installLogPath,
2064
2580
  legacyInstallLogPath,
2065
- buildAssetSpec,
2066
- resolveRules,
2067
- ALL_RULES,
2068
- TRACK_AGENTS,
2069
- RETIRED_AGENTS,
2070
- RETIRED_PATHS,
2071
- RETIRED_AGENT_IDS,
2072
- isCliNeutralTarget,
2073
- buildManifest,
2581
+ estimateTokens,
2582
+ resolveBundleRoot,
2583
+ extractFrontmatter,
2584
+ assetCostRows,
2585
+ residentCost,
2586
+ summarizeContextCost,
2587
+ formatResidentCostLine,
2588
+ formatResidentCostBlock,
2074
2589
  CATEGORIES,
2075
2590
  CATEGORY_TITLES
2076
2591
  };
2077
- //# sourceMappingURL=chunk-3G34MEGZ.js.map
2592
+ //# sourceMappingURL=chunk-X7NSQHXL.js.map