@uzysjung/agent-harness 26.153.0 → 26.154.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.
@@ -70,6 +70,14 @@ var DEFAULT_OPTIONS = {
70
70
  withPrune: false,
71
71
  withCodexTrust: false
72
72
  };
73
+ var UPDATE_GROUPS = [
74
+ "skills",
75
+ "new-skills",
76
+ "rules",
77
+ "anchor",
78
+ "hooks",
79
+ "external"
80
+ ];
73
81
 
74
82
  // src/external-assets.ts
75
83
  init_esm_shims();
@@ -1120,138 +1128,515 @@ function filterApplicableAssets(assets, ctx) {
1120
1128
  return assets.filter((a) => shouldInstallAsset(a, ctx));
1121
1129
  }
1122
1130
 
1123
- // src/manifest.ts
1131
+ // src/context-cost.ts
1124
1132
  init_esm_shims();
1133
+ import { existsSync, readFileSync } from "fs";
1134
+ import { join, resolve } from "path";
1135
+ import { fileURLToPath as fileURLToPath2 } from "url";
1125
1136
 
1126
- // src/install-log.ts
1137
+ // src/project-claude-merge.ts
1127
1138
  init_esm_shims();
1128
- import { createHash } from "crypto";
1129
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
1130
- import { dirname as dirname2, join as join2 } from "path";
1139
+ var HARNESS_ANCHOR_FILE = "CLAUDE-uzys-harness.md";
1140
+ var HARNESS_IMPORT_LINE = `@${HARNESS_ANCHOR_FILE}`;
1141
+ var IMPORT_MARKER_START = "<!-- uzys-harness:import:start -->";
1142
+ var IMPORT_MARKER_END = "<!-- uzys-harness:import:end -->";
1143
+ function renderContinuousSkillsNote(selectedInternalSkills) {
1144
+ const lines = CONTINUOUS_SKILLS.filter((s) => selectedInternalSkills.includes(s.id)).map(
1145
+ (s) => `- \`${s.id}\` \u2014 ${s.whenToApply}.`
1146
+ );
1147
+ if (lines.length === 0) return "";
1148
+ return [
1149
+ "## Skills that apply continuously",
1150
+ "",
1151
+ "These installed skills apply to every response or delegation; no prompt ever looks like their job, so open them from this list:",
1152
+ "",
1153
+ ...lines
1154
+ ].join("\n");
1155
+ }
1156
+ function withContinuousSkillsNote(scaffold, selectedInternalSkills) {
1157
+ const note = renderContinuousSkillsNote(selectedInternalSkills);
1158
+ return note ? `${scaffold}
1131
1159
 
1132
- // src/fs-ops.ts
1133
- init_esm_shims();
1134
- import {
1135
- copyFileSync,
1136
- cpSync,
1137
- existsSync,
1138
- constants as fsConstants,
1139
- mkdirSync,
1140
- readdirSync,
1141
- readFileSync,
1142
- realpathSync,
1143
- renameSync,
1144
- rmdirSync
1145
- } from "fs";
1146
- import { dirname, join } from "path";
1147
- function ensureDir(path2) {
1148
- mkdirSync(path2, { recursive: true });
1160
+ ${note}` : scaffold;
1149
1161
  }
1150
- function copyFile(source, target) {
1151
- if (!existsSync(source)) {
1152
- throw new Error(`Source not found: ${source}`);
1153
- }
1154
- mkdirSync(dirname(target), { recursive: true });
1155
- copyFileSync(source, target);
1162
+ function importBlock(continuousSkills) {
1163
+ const note = renderContinuousSkillsNote(continuousSkills);
1164
+ const body = note ? `${HARNESS_IMPORT_LINE}
1165
+
1166
+ ${note}` : HARNESS_IMPORT_LINE;
1167
+ return `${IMPORT_MARKER_START}
1168
+ ${body}
1169
+ ${IMPORT_MARKER_END}`;
1156
1170
  }
1157
- function copyDir(source, target, foreignOf) {
1158
- if (!existsSync(source)) {
1159
- throw new Error(`Source dir not found: ${source}`);
1171
+ var TRACK_DISPLAY_NAMES = {
1172
+ tooling: "Tooling",
1173
+ "csr-supabase": "CSR Supabase",
1174
+ "csr-fastify": "CSR Fastify",
1175
+ "csr-fastapi": "CSR FastAPI",
1176
+ "ssr-htmx": "SSR HTMX",
1177
+ "ssr-nextjs": "SSR Next.js",
1178
+ data: "Data",
1179
+ executive: "Executive",
1180
+ full: "Full",
1181
+ "project-management": "Project Management",
1182
+ "growth-marketing": "Growth Marketing"
1183
+ };
1184
+ var FULL_EXPANSION = TRACKS.filter((t) => t !== "full");
1185
+ var FILL_SECTIONS = [
1186
+ "identity",
1187
+ "stack",
1188
+ "architecture",
1189
+ "installed-assets",
1190
+ "boundaries",
1191
+ "verify"
1192
+ ];
1193
+ var FILL_SPECS = {
1194
+ identity: {
1195
+ title: "Identity & Purpose",
1196
+ 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.`,
1197
+ placeholder: "what this project is, who it is for, and why it exists"
1198
+ },
1199
+ stack: {
1200
+ title: "Stack & Commands",
1201
+ 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.",
1202
+ placeholder: "languages, runtimes, package manager, and the install/build/test/run commands"
1203
+ },
1204
+ architecture: {
1205
+ title: "Architecture & Layout",
1206
+ 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.",
1207
+ placeholder: "where things live, the entry points, and how data flows between layers"
1208
+ },
1209
+ "installed-assets": {
1210
+ title: "Installed Harness Assets",
1211
+ 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.",
1212
+ placeholder: "the installed rules/skills/agents/commands and when to use each"
1213
+ },
1214
+ boundaries: {
1215
+ title: "Boundaries \u2014 Always / Ask First / Never",
1216
+ 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.`,
1217
+ placeholder: "what to always do, ask before doing, and never do in this repo"
1218
+ },
1219
+ verify: {
1220
+ title: "Verification Gate",
1221
+ 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.',
1222
+ placeholder: "the single command that proves a change is safe, and the done bar"
1160
1223
  }
1161
- mkdirSync(target, { recursive: true });
1162
- const skipped = [];
1163
- for (const rel of listFilesRecursive(source)) {
1164
- const foreign = foreignOf(rel);
1165
- if (foreign !== null) {
1166
- skipped.push(foreign);
1167
- continue;
1168
- }
1169
- copyFile(join(source, rel), join(target, rel));
1224
+ };
1225
+ function anchorLine(surface) {
1226
+ if (surface === "claude") {
1227
+ 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**.`;
1170
1228
  }
1171
- return skipped;
1229
+ const self = surface === "antigravity-rule" ? "`.agents/rules/uzys-harness.md`" : "`AGENTS.md`";
1230
+ 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.`;
1172
1231
  }
1173
- var MAX_BACKUP_CLAIMS = 100;
1174
- function claimBackupPath(base, claim) {
1175
- for (let n = 1; n <= MAX_BACKUP_CLAIMS; n++) {
1176
- const candidate = n === 1 ? base : `${base}-${n}`;
1177
- try {
1178
- claim(candidate);
1179
- return candidate;
1180
- } catch (err) {
1181
- if (err.code !== "EEXIST") throw err;
1182
- }
1183
- }
1184
- throw new Error(`Cannot allocate a backup path for ${base} (${MAX_BACKUP_CLAIMS} taken)`);
1232
+ function scaffoldBanner(surface) {
1233
+ return [
1234
+ "> \u2699\uFE0F **SCAFFOLD \u2014 not filled in yet.** The sections below are a fill-in template for THIS project, not verified facts.",
1235
+ "> 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.",
1236
+ anchorLine(surface)
1237
+ ].join("\n");
1185
1238
  }
1186
- function backupDir(target, now = /* @__PURE__ */ new Date()) {
1187
- if (!existsSync(target)) {
1188
- return null;
1189
- }
1190
- const backup = claimBackupPath(`${target}.backup-${formatStamp(now)}`, (c) => mkdirSync(c));
1191
- rmdirSync(backup);
1192
- renameSync(target, backup);
1193
- return backup;
1239
+ function renderFillScaffold(surface = "claude") {
1240
+ const blocks = FILL_SECTIONS.map((id) => {
1241
+ const spec = FILL_SPECS[id];
1242
+ return `## ${spec.title}
1243
+
1244
+ <!-- FILL:${id} \u2014 ${spec.prompt} -->
1245
+
1246
+ _(not filled yet \u2014 ${spec.placeholder})_`;
1247
+ });
1248
+ return `${scaffoldBanner(surface)}
1249
+
1250
+ ${blocks.join("\n\n")}`;
1194
1251
  }
1195
- function copyBackupDir(target, now = /* @__PURE__ */ new Date()) {
1196
- if (!existsSync(target)) {
1197
- return null;
1198
- }
1199
- const backup = claimBackupPath(`${target}.backup-${formatStamp(now)}`, (c) => mkdirSync(c));
1200
- cpSync(realpathSync(target), backup, { recursive: true });
1201
- return backup;
1252
+ function mergeProjectClaude(opts) {
1253
+ const expanded = expandTracks(opts.tracks);
1254
+ const trackList = expanded.map((t) => TRACK_DISPLAY_NAMES[t]).join(", ");
1255
+ const header = `# ${opts.projectName}
1256
+
1257
+ > Active track(s): ${trackList}`;
1258
+ return `${header}
1259
+
1260
+ ${renderFillScaffold()}
1261
+ `;
1202
1262
  }
1203
- function backupFileIfChanged(target, newContent, now = /* @__PURE__ */ new Date()) {
1204
- if (!existsSync(target)) {
1205
- return null;
1263
+ function upsertHarnessImport(existing, opts) {
1264
+ const block = importBlock(opts.continuousSkills ?? []);
1265
+ if (existing === null) {
1266
+ return `${mergeProjectClaude(opts)}
1267
+ ${block}
1268
+ `;
1206
1269
  }
1207
- if (readFileSync(target, "utf-8") === newContent) {
1208
- return null;
1270
+ const start = existing.indexOf(IMPORT_MARKER_START);
1271
+ if (start !== -1) {
1272
+ const endAt = existing.indexOf(IMPORT_MARKER_END, start);
1273
+ if (endAt === -1) return existing;
1274
+ const current = existing.slice(start, endAt + IMPORT_MARKER_END.length);
1275
+ if (current === block) return existing;
1276
+ return `${existing.slice(0, start)}${block}${existing.slice(endAt + IMPORT_MARKER_END.length)}`;
1209
1277
  }
1210
- return backupFile(target, now);
1278
+ if (hasHarnessImport(existing)) {
1279
+ return existing;
1280
+ }
1281
+ const body = existing.endsWith("\n") ? existing : `${existing}
1282
+ `;
1283
+ return `${body}
1284
+ ${block}
1285
+ `;
1211
1286
  }
1212
- function backupFile(target, now = /* @__PURE__ */ new Date()) {
1213
- return claimBackupPath(
1214
- `${target}.backup-${formatStamp(now)}`,
1215
- (c) => copyFileSync(target, c, fsConstants.COPYFILE_EXCL)
1216
- );
1287
+ function stripHarnessImport(text) {
1288
+ const start = text.indexOf(IMPORT_MARKER_START);
1289
+ if (start === -1) return null;
1290
+ const endAt = text.indexOf(IMPORT_MARKER_END, start);
1291
+ if (endAt === -1) return null;
1292
+ const before = text.slice(0, start).replace(/\n+$/, "\n");
1293
+ const after = text.slice(endAt + IMPORT_MARKER_END.length).replace(/^\n+/, "");
1294
+ return `${before}${after}`;
1217
1295
  }
1218
- function listFilesRecursive(dir, prefix = "") {
1219
- if (!existsSync(dir)) return [];
1220
- const out = [];
1221
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1222
- const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
1223
- if (entry.isDirectory()) {
1224
- out.push(...listFilesRecursive(join(dir, entry.name), rel));
1225
- } else if (entry.isFile()) {
1226
- out.push(rel);
1296
+ function hasHarnessImport(text) {
1297
+ if (text.includes(IMPORT_MARKER_START)) return true;
1298
+ let inFence = false;
1299
+ for (const line of text.split("\n")) {
1300
+ if (/^\s*(?:```|~~~)/.test(line)) {
1301
+ inFence = !inFence;
1302
+ continue;
1227
1303
  }
1304
+ if (!inFence && line.trim() === HARNESS_IMPORT_LINE) return true;
1228
1305
  }
1229
- return out;
1230
- }
1231
- function formatStamp(now) {
1232
- return now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z").slice(0, 15);
1306
+ return false;
1233
1307
  }
1234
- function ensureProjectSkeleton(projectDir) {
1235
- const dirs = [
1236
- ".claude/commands/uzys",
1237
- ".claude/rules",
1238
- ".claude/skills",
1239
- ".claude/agents",
1240
- ".claude/hooks",
1241
- "docs/decisions"
1242
- ];
1243
- for (const d of dirs) {
1244
- mkdirSync(join(projectDir, d), { recursive: true });
1308
+ function expandTracks(tracks) {
1309
+ if (tracks.includes("full")) {
1310
+ return FULL_EXPANSION;
1245
1311
  }
1312
+ return tracks;
1246
1313
  }
1247
1314
 
1248
- // src/install-log.ts
1249
- var INSTALL_LOG_FILENAME = ".harness-install.json";
1250
- var INSTALL_LOG_DIR = ".uzys-agent-harness";
1251
- var LEGACY_INSTALL_LOG_DIR = ".claude";
1252
- var INSTALL_LOG_VERSION = 1;
1253
- var POLICY_DIRS = [
1254
- { dir: "rules", ext: ".md" },
1315
+ // src/context-cost.ts
1316
+ var CHARS_PER_TOKEN = 4;
1317
+ function estimateTokens(chars) {
1318
+ return Math.ceil(chars / CHARS_PER_TOKEN);
1319
+ }
1320
+ function resolveBundleRoot() {
1321
+ return resolve(fileURLToPath2(new URL(".", import.meta.url)), "..");
1322
+ }
1323
+ function extractFrontmatter(content) {
1324
+ const m = content.match(/^?---\r?\n([\s\S]*?)\r?\n---/);
1325
+ return m?.[1] ?? null;
1326
+ }
1327
+ function assetDescriptorTokens(assetId, root = resolveBundleRoot()) {
1328
+ const asset = EXTERNAL_ASSETS.find((a) => a.id === assetId);
1329
+ if (!asset || asset.method.kind !== "internal") return null;
1330
+ const skillMd = join(root, "templates", "skills", asset.method.key, "SKILL.md");
1331
+ if (!existsSync(skillMd)) return null;
1332
+ const fm = extractFrontmatter(readFileSync(skillMd, "utf8"));
1333
+ return fm === null ? null : estimateTokens(fm.length);
1334
+ }
1335
+ function assetBodyTokens(assetId, root = resolveBundleRoot()) {
1336
+ const asset = EXTERNAL_ASSETS.find((a) => a.id === assetId);
1337
+ if (!asset || asset.method.kind !== "internal") return null;
1338
+ const skillMd = join(root, "templates", "skills", asset.method.key, "SKILL.md");
1339
+ if (!existsSync(skillMd)) return null;
1340
+ const content = readFileSync(skillMd, "utf8");
1341
+ const fm = extractFrontmatter(content);
1342
+ if (fm === null) return estimateTokens(content.length);
1343
+ const close = content.indexOf("\n---", content.indexOf(fm) + fm.length);
1344
+ const body = close === -1 ? "" : content.slice(close + "\n---".length);
1345
+ return estimateTokens(body.trim().length);
1346
+ }
1347
+ function assetCostRows(assetIds, root = resolveBundleRoot()) {
1348
+ return assetIds.map((id) => ({
1349
+ id,
1350
+ descriptorTokens: assetDescriptorTokens(id, root),
1351
+ bodyTokens: assetBodyTokens(id, root)
1352
+ })).sort((a, b) => (b.bodyTokens ?? -1) - (a.bodyTokens ?? -1));
1353
+ }
1354
+ function makeResidentCost(base) {
1355
+ return {
1356
+ ...base,
1357
+ perSkillDescriptor: base.perSkillDescriptor ?? {},
1358
+ total: base.rules + base.projectClaudeMd + base.skillDescriptors + base.agentDescriptors,
1359
+ directive: {
1360
+ items: base.items.rules + base.items.claudeMd,
1361
+ tokens: base.rules + base.projectClaudeMd
1362
+ },
1363
+ firing: {
1364
+ items: base.items.skills + base.items.agents,
1365
+ tokens: base.skillDescriptors + base.agentDescriptors
1366
+ }
1367
+ };
1368
+ }
1369
+ function fileTokens(path2) {
1370
+ return existsSync(path2) ? estimateTokens(readFileSync(path2, "utf8").trim().length) : 0;
1371
+ }
1372
+ function descriptorTokens(path2) {
1373
+ if (!existsSync(path2)) return 0;
1374
+ const fm = extractFrontmatter(readFileSync(path2, "utf8"));
1375
+ return fm === null ? 0 : estimateTokens(fm.length);
1376
+ }
1377
+ function landsOnDisk(target, cli) {
1378
+ if (target.startsWith(".claude/agents/")) return cli.includes("claude");
1379
+ return true;
1380
+ }
1381
+ function residentCost(entries, root = resolveBundleRoot()) {
1382
+ const tpl = (source) => join(root, "templates", source);
1383
+ const measured = (e) => e.file ?? tpl(e.source);
1384
+ const perSkill = {};
1385
+ let rules = 0;
1386
+ let skillDescriptors = 0;
1387
+ let agentDescriptors = 0;
1388
+ let ruleItems = 0;
1389
+ let skillItems = 0;
1390
+ let agentItems = 0;
1391
+ for (const e of entries) {
1392
+ if (e.target.startsWith(".claude/rules/")) {
1393
+ rules += fileTokens(measured(e));
1394
+ ruleItems += 1;
1395
+ } else if (e.target.startsWith(".claude/agents/")) {
1396
+ agentDescriptors += descriptorTokens(measured(e));
1397
+ agentItems += 1;
1398
+ } else if (e.target.startsWith(".claude/skills/")) {
1399
+ const t = descriptorTokens(join(measured(e), "SKILL.md"));
1400
+ skillDescriptors += t;
1401
+ skillItems += 1;
1402
+ const id = /\.claude\/skills\/([^/]+)/.exec(e.target)?.[1];
1403
+ if (id !== void 0) perSkill[id] = (perSkill[id] ?? 0) + t;
1404
+ }
1405
+ }
1406
+ const harnessAnchor = fileTokens(join(root, "templates", "CLAUDE.md"));
1407
+ const projectScaffold = estimateTokens(
1408
+ withContinuousSkillsNote(
1409
+ renderFillScaffold(),
1410
+ CONTINUOUS_SKILLS.map((s) => s.id)
1411
+ ).trim().length
1412
+ );
1413
+ const projectClaudeMd = harnessAnchor + projectScaffold;
1414
+ const claudeMdItems = (harnessAnchor > 0 ? 1 : 0) + (projectScaffold > 0 ? 1 : 0);
1415
+ return makeResidentCost({
1416
+ rules,
1417
+ projectClaudeMd,
1418
+ skillDescriptors,
1419
+ agentDescriptors,
1420
+ perSkillDescriptor: perSkill,
1421
+ items: {
1422
+ rules: ruleItems,
1423
+ skills: skillItems,
1424
+ agents: agentItems,
1425
+ claudeMd: claudeMdItems,
1426
+ total: ruleItems + skillItems + agentItems + claudeMdItems
1427
+ }
1428
+ });
1429
+ }
1430
+ function summarizeContextCost(assetIds, root = resolveBundleRoot()) {
1431
+ let measuredTokens = 0;
1432
+ let measuredCount = 0;
1433
+ let unmeasuredCount = 0;
1434
+ for (const id of assetIds) {
1435
+ const tokens = assetDescriptorTokens(id, root);
1436
+ if (tokens === null) unmeasuredCount += 1;
1437
+ else {
1438
+ measuredTokens += tokens;
1439
+ measuredCount += 1;
1440
+ }
1441
+ }
1442
+ return { measuredTokens, measuredCount, unmeasuredCount };
1443
+ }
1444
+ function formatResidentCostLine(r, unmeasuredCount) {
1445
+ if (r.total === 0) return null;
1446
+ const parts = [
1447
+ `directives ${r.directive.items} ~${r.directive.tokens}`,
1448
+ `[rules ${r.items.rules} ~${r.rules} \xB7 CLAUDE.md ${r.items.claudeMd} ~${r.projectClaudeMd}]`,
1449
+ `triggers ${r.firing.items} ~${r.firing.tokens}`,
1450
+ `[skills ${r.items.skills} ~${r.skillDescriptors} \xB7 agents ${r.items.agents} ~${r.agentDescriptors}]`
1451
+ ].join(" \xB7 ");
1452
+ const external = unmeasuredCount > 0 ? ` \xB7 ${unmeasuredCount} external asset${unmeasuredCount === 1 ? "" : "s"} unmeasured` : "";
1453
+ return `session-start context cost: ${r.items.total} items resident \xB7 ~${r.total} tokens/session (${parts}${external})`;
1454
+ }
1455
+ var RESIDENT_ROWS = [
1456
+ {
1457
+ // ADR-083 — 축 소계를 **행으로** 낸다. 소계가 없으면 읽는 사람이 네 줄을 머리로 더해야
1458
+ // 하고, 그러면 "무엇을 줄여야 하나"가 표에서 안 보인다.
1459
+ label: "\uC9C0\uC2DC\uBB38",
1460
+ gap: 13,
1461
+ count: (r) => r.directive.items,
1462
+ tokens: (r) => r.directive.tokens
1463
+ },
1464
+ { label: " rules", gap: 12, count: (r) => r.items.rules, tokens: (r) => r.rules },
1465
+ {
1466
+ // 한 행에 두 파일(앵커 + 스캐폴드). 개수 열이 2 를 뱉어 그 사실을 드러낸다 — 라벨로
1467
+ // 하나만 이름 붙이면 v26.140.0 이전과 같은 거짓 라벨이 된다.
1468
+ label: " CLAUDE.md",
1469
+ gap: 8,
1470
+ count: (r) => r.items.claudeMd,
1471
+ tokens: (r) => r.projectClaudeMd
1472
+ },
1473
+ {
1474
+ label: "\uBC1C\uD654 \uD45C\uBA74",
1475
+ gap: 10,
1476
+ count: (r) => r.firing.items,
1477
+ tokens: (r) => r.firing.tokens
1478
+ },
1479
+ {
1480
+ label: " skill descriptors",
1481
+ gap: 0,
1482
+ count: (r) => r.items.skills,
1483
+ tokens: (r) => r.skillDescriptors
1484
+ },
1485
+ {
1486
+ label: " agent descriptors",
1487
+ gap: 0,
1488
+ count: (r) => r.items.agents,
1489
+ tokens: (r) => r.agentDescriptors
1490
+ }
1491
+ ];
1492
+ function formatResidentCostBlock(r) {
1493
+ const n = (v) => String(v).padStart(3);
1494
+ return [
1495
+ " \uAC1C\uC218 \uD1A0\uD070",
1496
+ ...RESIDENT_ROWS.map(
1497
+ (row) => ` ${row.label}${" ".repeat(row.gap)}${n(row.count(r))}\uAC1C ~${row.tokens(r)}`
1498
+ ),
1499
+ " \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",
1500
+ ` \uC0C1\uC8FC \uD569\uACC4 ${n(r.items.total)}\uAC1C \uC0C1\uC8FC \xB7 ~${r.total} tokens/\uC138\uC158`,
1501
+ "",
1502
+ " \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",
1503
+ " '\uBD88\uD544\uC694\uD55C \uC9C0\uC2DC\uBB38\uC774 \uC788\uB098'\uB85C \uD55C\uB2E4. ratchet \uC740 \uC870\uC6A9\uD55C \uC99D\uAC00\uB9CC \uB9C9\uB294\uB2E4.",
1504
+ " \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."
1505
+ ];
1506
+ }
1507
+
1508
+ // src/manifest.ts
1509
+ init_esm_shims();
1510
+
1511
+ // src/install-log.ts
1512
+ init_esm_shims();
1513
+ import { createHash } from "crypto";
1514
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync } from "fs";
1515
+ import { dirname as dirname2, join as join3 } from "path";
1516
+
1517
+ // src/fs-ops.ts
1518
+ init_esm_shims();
1519
+ import {
1520
+ copyFileSync,
1521
+ cpSync,
1522
+ existsSync as existsSync2,
1523
+ constants as fsConstants,
1524
+ mkdirSync,
1525
+ readdirSync,
1526
+ readFileSync as readFileSync2,
1527
+ realpathSync,
1528
+ renameSync,
1529
+ rmdirSync
1530
+ } from "fs";
1531
+ import { dirname, join as join2 } from "path";
1532
+ function ensureDir(path2) {
1533
+ mkdirSync(path2, { recursive: true });
1534
+ }
1535
+ function copyFile(source, target) {
1536
+ if (!existsSync2(source)) {
1537
+ throw new Error(`Source not found: ${source}`);
1538
+ }
1539
+ mkdirSync(dirname(target), { recursive: true });
1540
+ copyFileSync(source, target);
1541
+ }
1542
+ function copyDir(source, target, foreignOf) {
1543
+ if (!existsSync2(source)) {
1544
+ throw new Error(`Source dir not found: ${source}`);
1545
+ }
1546
+ mkdirSync(target, { recursive: true });
1547
+ const skipped = [];
1548
+ for (const rel of listFilesRecursive(source)) {
1549
+ const foreign = foreignOf(rel);
1550
+ if (foreign !== null) {
1551
+ skipped.push(foreign);
1552
+ continue;
1553
+ }
1554
+ copyFile(join2(source, rel), join2(target, rel));
1555
+ }
1556
+ return skipped;
1557
+ }
1558
+ var MAX_BACKUP_CLAIMS = 100;
1559
+ function claimBackupPath(base, claim) {
1560
+ for (let n = 1; n <= MAX_BACKUP_CLAIMS; n++) {
1561
+ const candidate = n === 1 ? base : `${base}-${n}`;
1562
+ try {
1563
+ claim(candidate);
1564
+ return candidate;
1565
+ } catch (err) {
1566
+ if (err.code !== "EEXIST") throw err;
1567
+ }
1568
+ }
1569
+ throw new Error(`Cannot allocate a backup path for ${base} (${MAX_BACKUP_CLAIMS} taken)`);
1570
+ }
1571
+ function backupDir(target, now = /* @__PURE__ */ new Date()) {
1572
+ if (!existsSync2(target)) {
1573
+ return null;
1574
+ }
1575
+ const backup = claimBackupPath(`${target}.backup-${formatStamp(now)}`, (c) => mkdirSync(c));
1576
+ rmdirSync(backup);
1577
+ renameSync(target, backup);
1578
+ return backup;
1579
+ }
1580
+ function copyBackupDir(target, now = /* @__PURE__ */ new Date()) {
1581
+ if (!existsSync2(target)) {
1582
+ return null;
1583
+ }
1584
+ const backup = claimBackupPath(`${target}.backup-${formatStamp(now)}`, (c) => mkdirSync(c));
1585
+ cpSync(realpathSync(target), backup, { recursive: true });
1586
+ return backup;
1587
+ }
1588
+ function backupFileIfChanged(target, newContent, now = /* @__PURE__ */ new Date()) {
1589
+ if (!existsSync2(target)) {
1590
+ return null;
1591
+ }
1592
+ if (readFileSync2(target, "utf-8") === newContent) {
1593
+ return null;
1594
+ }
1595
+ return backupFile(target, now);
1596
+ }
1597
+ function backupFile(target, now = /* @__PURE__ */ new Date()) {
1598
+ return claimBackupPath(
1599
+ `${target}.backup-${formatStamp(now)}`,
1600
+ (c) => copyFileSync(target, c, fsConstants.COPYFILE_EXCL)
1601
+ );
1602
+ }
1603
+ function listFilesRecursive(dir, prefix = "") {
1604
+ if (!existsSync2(dir)) return [];
1605
+ const out = [];
1606
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1607
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
1608
+ if (entry.isDirectory()) {
1609
+ out.push(...listFilesRecursive(join2(dir, entry.name), rel));
1610
+ } else if (entry.isFile()) {
1611
+ out.push(rel);
1612
+ }
1613
+ }
1614
+ return out;
1615
+ }
1616
+ function formatStamp(now) {
1617
+ return now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z").slice(0, 15);
1618
+ }
1619
+ function ensureProjectSkeleton(projectDir) {
1620
+ const dirs = [
1621
+ ".claude/commands/uzys",
1622
+ ".claude/rules",
1623
+ ".claude/skills",
1624
+ ".claude/agents",
1625
+ ".claude/hooks",
1626
+ "docs/decisions"
1627
+ ];
1628
+ for (const d of dirs) {
1629
+ mkdirSync(join2(projectDir, d), { recursive: true });
1630
+ }
1631
+ }
1632
+
1633
+ // src/install-log.ts
1634
+ var INSTALL_LOG_FILENAME = ".harness-install.json";
1635
+ var INSTALL_LOG_DIR = ".uzys-agent-harness";
1636
+ var LEGACY_INSTALL_LOG_DIR = ".claude";
1637
+ var INSTALL_LOG_VERSION = 1;
1638
+ var POLICY_DIRS = [
1639
+ { dir: "rules", ext: ".md" },
1255
1640
  { dir: "agents", ext: ".md" },
1256
1641
  { dir: "commands/uzys", ext: ".md" },
1257
1642
  { dir: "hooks", ext: ".sh" }
@@ -1371,247 +1756,69 @@ function isHarnessOwned(baseline, key, current) {
1371
1756
  function mergeExternalFiles(projectDir, previous, current) {
1372
1757
  const byPath = /* @__PURE__ */ new Map();
1373
1758
  for (const file of [...previous ?? [], ...current]) byPath.set(file.path, file);
1374
- return [...byPath.values()].filter((f) => existsSync2(join2(projectDir, f.path)));
1759
+ return [...byPath.values()].filter((f) => existsSync3(join3(projectDir, f.path)));
1375
1760
  }
1376
- function collectSkillHashes(projectDir) {
1377
- const skillsDir = join2(projectDir, ".claude/skills");
1378
- return listFilesRecursive(skillsDir).map((rel) => ({
1761
+ function collectSkillHashes(projectDir, templatesDir) {
1762
+ const skillsDir = join3(projectDir, ".claude/skills");
1763
+ return listFilesRecursive(skillsDir).filter((rel) => existsSync3(join3(templatesDir, "skills", rel))).map((rel) => ({
1379
1764
  path: rel,
1380
- sha256: hashContent(readFileSync2(join2(skillsDir, rel), "utf8"))
1765
+ sha256: hashContent(readFileSync3(join3(skillsDir, rel), "utf8"))
1381
1766
  }));
1382
1767
  }
1383
1768
  function collectPolicyHashes(projectDir, templatesDir) {
1384
1769
  const out = [];
1385
1770
  for (const { dir, ext } of POLICY_DIRS) {
1386
- const targetDir = join2(projectDir, ".claude", dir);
1387
- const sourceDir = join2(templatesDir, dir);
1771
+ const targetDir = join3(projectDir, ".claude", dir);
1772
+ const sourceDir = join3(templatesDir, dir);
1388
1773
  for (const rel of listFilesRecursive(targetDir)) {
1389
1774
  if (!rel.endsWith(ext)) continue;
1390
- if (!existsSync2(join2(sourceDir, rel))) continue;
1775
+ if (!existsSync3(join3(sourceDir, rel))) continue;
1391
1776
  out.push({
1392
1777
  path: `${dir}/${rel}`,
1393
- sha256: hashContent(readFileSync2(join2(targetDir, rel), "utf8"))
1778
+ sha256: hashContent(readFileSync3(join3(targetDir, rel), "utf8"))
1394
1779
  });
1395
1780
  }
1396
- }
1397
- return out;
1398
- }
1399
- function writeInstallLog(projectDir, log) {
1400
- const path2 = installLogPath(projectDir);
1401
- mkdirSync2(dirname2(path2), { recursive: true });
1402
- writeFileSync(path2, `${JSON.stringify(log, null, 2)}
1403
- `, "utf8");
1404
- migrateAwayLegacyLog(projectDir);
1405
- return path2;
1406
- }
1407
- function migrateAwayLegacyLog(projectDir) {
1408
- const legacy = legacyInstallLogPath(projectDir);
1409
- if (!existsSync2(legacy)) return;
1410
- try {
1411
- rmSync(legacy);
1412
- } catch {
1413
- }
1414
- }
1415
- function readInstallLog(projectDir) {
1416
- const path2 = [installLogPath(projectDir), legacyInstallLogPath(projectDir)].find(
1417
- (p) => existsSync2(p)
1418
- );
1419
- if (!path2) return null;
1420
- try {
1421
- const parsed = JSON.parse(readFileSync2(path2, "utf8"));
1422
- if (Array.isArray(parsed.assets)) {
1423
- parsed.assets = parsed.assets.map(
1424
- (a) => a.method === "npm-global" ? { ...a, method: "npm" } : a
1425
- );
1426
- }
1427
- return parsed;
1428
- } catch {
1429
- return null;
1430
- }
1431
- }
1432
- function installLogPath(projectDir) {
1433
- return join2(projectDir, INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
1434
- }
1435
- function legacyInstallLogPath(projectDir) {
1436
- return join2(projectDir, LEGACY_INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
1437
- }
1438
-
1439
- // src/project-claude-merge.ts
1440
- init_esm_shims();
1441
- var HARNESS_ANCHOR_FILE = "CLAUDE-uzys-harness.md";
1442
- var HARNESS_IMPORT_LINE = `@${HARNESS_ANCHOR_FILE}`;
1443
- var IMPORT_MARKER_START = "<!-- uzys-harness:import:start -->";
1444
- var IMPORT_MARKER_END = "<!-- uzys-harness:import:end -->";
1445
- function renderContinuousSkillsNote(selectedInternalSkills) {
1446
- const lines = CONTINUOUS_SKILLS.filter((s) => selectedInternalSkills.includes(s.id)).map(
1447
- (s) => `- \`${s.id}\` \u2014 ${s.whenToApply}.`
1448
- );
1449
- if (lines.length === 0) return "";
1450
- return [
1451
- "## Skills that apply continuously",
1452
- "",
1453
- "These installed skills apply to every response or delegation; no prompt ever looks like their job, so open them from this list:",
1454
- "",
1455
- ...lines
1456
- ].join("\n");
1457
- }
1458
- function withContinuousSkillsNote(scaffold, selectedInternalSkills) {
1459
- const note = renderContinuousSkillsNote(selectedInternalSkills);
1460
- return note ? `${scaffold}
1461
-
1462
- ${note}` : scaffold;
1463
- }
1464
- function importBlock(continuousSkills) {
1465
- const note = renderContinuousSkillsNote(continuousSkills);
1466
- const body = note ? `${HARNESS_IMPORT_LINE}
1467
-
1468
- ${note}` : HARNESS_IMPORT_LINE;
1469
- return `${IMPORT_MARKER_START}
1470
- ${body}
1471
- ${IMPORT_MARKER_END}`;
1472
- }
1473
- var TRACK_DISPLAY_NAMES = {
1474
- tooling: "Tooling",
1475
- "csr-supabase": "CSR Supabase",
1476
- "csr-fastify": "CSR Fastify",
1477
- "csr-fastapi": "CSR FastAPI",
1478
- "ssr-htmx": "SSR HTMX",
1479
- "ssr-nextjs": "SSR Next.js",
1480
- data: "Data",
1481
- executive: "Executive",
1482
- full: "Full",
1483
- "project-management": "Project Management",
1484
- "growth-marketing": "Growth Marketing"
1485
- };
1486
- var FULL_EXPANSION = TRACKS.filter((t) => t !== "full");
1487
- var FILL_SECTIONS = [
1488
- "identity",
1489
- "stack",
1490
- "architecture",
1491
- "installed-assets",
1492
- "boundaries",
1493
- "verify"
1494
- ];
1495
- var FILL_SPECS = {
1496
- identity: {
1497
- title: "Identity & Purpose",
1498
- 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.`,
1499
- placeholder: "what this project is, who it is for, and why it exists"
1500
- },
1501
- stack: {
1502
- title: "Stack & Commands",
1503
- 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.",
1504
- placeholder: "languages, runtimes, package manager, and the install/build/test/run commands"
1505
- },
1506
- architecture: {
1507
- title: "Architecture & Layout",
1508
- 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.",
1509
- placeholder: "where things live, the entry points, and how data flows between layers"
1510
- },
1511
- "installed-assets": {
1512
- title: "Installed Harness Assets",
1513
- 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.",
1514
- placeholder: "the installed rules/skills/agents/commands and when to use each"
1515
- },
1516
- boundaries: {
1517
- title: "Boundaries \u2014 Always / Ask First / Never",
1518
- 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.`,
1519
- placeholder: "what to always do, ask before doing, and never do in this repo"
1520
- },
1521
- verify: {
1522
- title: "Verification Gate",
1523
- 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.',
1524
- placeholder: "the single command that proves a change is safe, and the done bar"
1525
- }
1526
- };
1527
- function anchorLine(surface) {
1528
- if (surface === "claude") {
1529
- 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**.`;
1530
- }
1531
- const self = surface === "antigravity-rule" ? "`.agents/rules/uzys-harness.md`" : "`AGENTS.md`";
1532
- 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.`;
1533
- }
1534
- function scaffoldBanner(surface) {
1535
- return [
1536
- "> \u2699\uFE0F **SCAFFOLD \u2014 not filled in yet.** The sections below are a fill-in template for THIS project, not verified facts.",
1537
- "> 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.",
1538
- anchorLine(surface)
1539
- ].join("\n");
1540
- }
1541
- function renderFillScaffold(surface = "claude") {
1542
- const blocks = FILL_SECTIONS.map((id) => {
1543
- const spec = FILL_SPECS[id];
1544
- return `## ${spec.title}
1545
-
1546
- <!-- FILL:${id} \u2014 ${spec.prompt} -->
1547
-
1548
- _(not filled yet \u2014 ${spec.placeholder})_`;
1549
- });
1550
- return `${scaffoldBanner(surface)}
1551
-
1552
- ${blocks.join("\n\n")}`;
1553
- }
1554
- function mergeProjectClaude(opts) {
1555
- const expanded = expandTracks(opts.tracks);
1556
- const trackList = expanded.map((t) => TRACK_DISPLAY_NAMES[t]).join(", ");
1557
- const header = `# ${opts.projectName}
1558
-
1559
- > Active track(s): ${trackList}`;
1560
- return `${header}
1561
-
1562
- ${renderFillScaffold()}
1563
- `;
1564
- }
1565
- function upsertHarnessImport(existing, opts) {
1566
- const block = importBlock(opts.continuousSkills ?? []);
1567
- if (existing === null) {
1568
- return `${mergeProjectClaude(opts)}
1569
- ${block}
1570
- `;
1571
- }
1572
- const start = existing.indexOf(IMPORT_MARKER_START);
1573
- if (start !== -1) {
1574
- const endAt = existing.indexOf(IMPORT_MARKER_END, start);
1575
- if (endAt === -1) return existing;
1576
- const current = existing.slice(start, endAt + IMPORT_MARKER_END.length);
1577
- if (current === block) return existing;
1578
- return `${existing.slice(0, start)}${block}${existing.slice(endAt + IMPORT_MARKER_END.length)}`;
1579
- }
1580
- if (hasHarnessImport(existing)) {
1581
- return existing;
1582
- }
1583
- const body = existing.endsWith("\n") ? existing : `${existing}
1584
- `;
1585
- return `${body}
1586
- ${block}
1587
- `;
1781
+ }
1782
+ return out;
1588
1783
  }
1589
- function stripHarnessImport(text) {
1590
- const start = text.indexOf(IMPORT_MARKER_START);
1591
- if (start === -1) return null;
1592
- const endAt = text.indexOf(IMPORT_MARKER_END, start);
1593
- if (endAt === -1) return null;
1594
- const before = text.slice(0, start).replace(/\n+$/, "\n");
1595
- const after = text.slice(endAt + IMPORT_MARKER_END.length).replace(/^\n+/, "");
1596
- return `${before}${after}`;
1784
+ function writeInstallLog(projectDir, log) {
1785
+ const path2 = installLogPath(projectDir);
1786
+ mkdirSync2(dirname2(path2), { recursive: true });
1787
+ writeFileSync(path2, `${JSON.stringify(log, null, 2)}
1788
+ `, "utf8");
1789
+ migrateAwayLegacyLog(projectDir);
1790
+ return path2;
1597
1791
  }
1598
- function hasHarnessImport(text) {
1599
- if (text.includes(IMPORT_MARKER_START)) return true;
1600
- let inFence = false;
1601
- for (const line of text.split("\n")) {
1602
- if (/^\s*(?:```|~~~)/.test(line)) {
1603
- inFence = !inFence;
1604
- continue;
1605
- }
1606
- if (!inFence && line.trim() === HARNESS_IMPORT_LINE) return true;
1792
+ function migrateAwayLegacyLog(projectDir) {
1793
+ const legacy = legacyInstallLogPath(projectDir);
1794
+ if (!existsSync3(legacy)) return;
1795
+ try {
1796
+ rmSync(legacy);
1797
+ } catch {
1607
1798
  }
1608
- return false;
1609
1799
  }
1610
- function expandTracks(tracks) {
1611
- if (tracks.includes("full")) {
1612
- return FULL_EXPANSION;
1800
+ function readInstallLog(projectDir) {
1801
+ const path2 = [installLogPath(projectDir), legacyInstallLogPath(projectDir)].find(
1802
+ (p) => existsSync3(p)
1803
+ );
1804
+ if (!path2) return null;
1805
+ try {
1806
+ const parsed = JSON.parse(readFileSync3(path2, "utf8"));
1807
+ if (Array.isArray(parsed.assets)) {
1808
+ parsed.assets = parsed.assets.map(
1809
+ (a) => a.method === "npm-global" ? { ...a, method: "npm" } : a
1810
+ );
1811
+ }
1812
+ return parsed;
1813
+ } catch {
1814
+ return null;
1613
1815
  }
1614
- return tracks;
1816
+ }
1817
+ function installLogPath(projectDir) {
1818
+ return join3(projectDir, INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
1819
+ }
1820
+ function legacyInstallLogPath(projectDir) {
1821
+ return join3(projectDir, LEGACY_INSTALL_LOG_DIR, INSTALL_LOG_FILENAME);
1615
1822
  }
1616
1823
 
1617
1824
  // src/manifest.ts
@@ -1893,199 +2100,6 @@ var CATEGORY_TITLES = {
1893
2100
  "ecc-suite": "\u{1F4E6} ECC Suite"
1894
2101
  };
1895
2102
 
1896
- // src/context-cost.ts
1897
- init_esm_shims();
1898
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1899
- import { join as join3, resolve } from "path";
1900
- import { fileURLToPath as fileURLToPath2 } from "url";
1901
- var CHARS_PER_TOKEN = 4;
1902
- function estimateTokens(chars) {
1903
- return Math.ceil(chars / CHARS_PER_TOKEN);
1904
- }
1905
- function resolveBundleRoot() {
1906
- return resolve(fileURLToPath2(new URL(".", import.meta.url)), "..");
1907
- }
1908
- function extractFrontmatter(content) {
1909
- const m = content.match(/^?---\r?\n([\s\S]*?)\r?\n---/);
1910
- return m?.[1] ?? null;
1911
- }
1912
- function assetDescriptorTokens(assetId, root = resolveBundleRoot()) {
1913
- const asset = EXTERNAL_ASSETS.find((a) => a.id === assetId);
1914
- if (!asset || asset.method.kind !== "internal") return null;
1915
- const skillMd = join3(root, "templates", "skills", asset.method.key, "SKILL.md");
1916
- if (!existsSync3(skillMd)) return null;
1917
- const fm = extractFrontmatter(readFileSync3(skillMd, "utf8"));
1918
- return fm === null ? null : estimateTokens(fm.length);
1919
- }
1920
- function assetBodyTokens(assetId, root = resolveBundleRoot()) {
1921
- const asset = EXTERNAL_ASSETS.find((a) => a.id === assetId);
1922
- if (!asset || asset.method.kind !== "internal") return null;
1923
- const skillMd = join3(root, "templates", "skills", asset.method.key, "SKILL.md");
1924
- if (!existsSync3(skillMd)) return null;
1925
- const content = readFileSync3(skillMd, "utf8");
1926
- const fm = extractFrontmatter(content);
1927
- if (fm === null) return estimateTokens(content.length);
1928
- const close = content.indexOf("\n---", content.indexOf(fm) + fm.length);
1929
- const body = close === -1 ? "" : content.slice(close + "\n---".length);
1930
- return estimateTokens(body.trim().length);
1931
- }
1932
- function assetCostRows(assetIds, root = resolveBundleRoot()) {
1933
- return assetIds.map((id) => ({
1934
- id,
1935
- descriptorTokens: assetDescriptorTokens(id, root),
1936
- bodyTokens: assetBodyTokens(id, root)
1937
- })).sort((a, b) => (b.bodyTokens ?? -1) - (a.bodyTokens ?? -1));
1938
- }
1939
- function makeResidentCost(base) {
1940
- return {
1941
- ...base,
1942
- perSkillDescriptor: base.perSkillDescriptor ?? {},
1943
- total: base.rules + base.projectClaudeMd + base.skillDescriptors + base.agentDescriptors,
1944
- directive: {
1945
- items: base.items.rules + base.items.claudeMd,
1946
- tokens: base.rules + base.projectClaudeMd
1947
- },
1948
- firing: {
1949
- items: base.items.skills + base.items.agents,
1950
- tokens: base.skillDescriptors + base.agentDescriptors
1951
- }
1952
- };
1953
- }
1954
- function fileTokens(path2) {
1955
- return existsSync3(path2) ? estimateTokens(readFileSync3(path2, "utf8").trim().length) : 0;
1956
- }
1957
- function descriptorTokens(path2) {
1958
- if (!existsSync3(path2)) return 0;
1959
- const fm = extractFrontmatter(readFileSync3(path2, "utf8"));
1960
- return fm === null ? 0 : estimateTokens(fm.length);
1961
- }
1962
- function residentCost(entries, root = resolveBundleRoot()) {
1963
- const tpl = (source) => join3(root, "templates", source);
1964
- const measured = (e) => e.file ?? tpl(e.source);
1965
- const perSkill = {};
1966
- let rules = 0;
1967
- let skillDescriptors = 0;
1968
- let agentDescriptors = 0;
1969
- let ruleItems = 0;
1970
- let skillItems = 0;
1971
- let agentItems = 0;
1972
- for (const e of entries) {
1973
- if (e.target.startsWith(".claude/rules/")) {
1974
- rules += fileTokens(measured(e));
1975
- ruleItems += 1;
1976
- } else if (e.target.startsWith(".claude/agents/")) {
1977
- agentDescriptors += descriptorTokens(measured(e));
1978
- agentItems += 1;
1979
- } else if (e.target.startsWith(".claude/skills/")) {
1980
- const t = descriptorTokens(join3(measured(e), "SKILL.md"));
1981
- skillDescriptors += t;
1982
- skillItems += 1;
1983
- const id = /\.claude\/skills\/([^/]+)/.exec(e.target)?.[1];
1984
- if (id !== void 0) perSkill[id] = (perSkill[id] ?? 0) + t;
1985
- }
1986
- }
1987
- const harnessAnchor = fileTokens(join3(root, "templates", "CLAUDE.md"));
1988
- const projectScaffold = estimateTokens(
1989
- withContinuousSkillsNote(
1990
- renderFillScaffold(),
1991
- CONTINUOUS_SKILLS.map((s) => s.id)
1992
- ).trim().length
1993
- );
1994
- const projectClaudeMd = harnessAnchor + projectScaffold;
1995
- const claudeMdItems = (harnessAnchor > 0 ? 1 : 0) + (projectScaffold > 0 ? 1 : 0);
1996
- return makeResidentCost({
1997
- rules,
1998
- projectClaudeMd,
1999
- skillDescriptors,
2000
- agentDescriptors,
2001
- perSkillDescriptor: perSkill,
2002
- items: {
2003
- rules: ruleItems,
2004
- skills: skillItems,
2005
- agents: agentItems,
2006
- claudeMd: claudeMdItems,
2007
- total: ruleItems + skillItems + agentItems + claudeMdItems
2008
- }
2009
- });
2010
- }
2011
- function summarizeContextCost(assetIds, root = resolveBundleRoot()) {
2012
- let measuredTokens = 0;
2013
- let measuredCount = 0;
2014
- let unmeasuredCount = 0;
2015
- for (const id of assetIds) {
2016
- const tokens = assetDescriptorTokens(id, root);
2017
- if (tokens === null) unmeasuredCount += 1;
2018
- else {
2019
- measuredTokens += tokens;
2020
- measuredCount += 1;
2021
- }
2022
- }
2023
- return { measuredTokens, measuredCount, unmeasuredCount };
2024
- }
2025
- function formatResidentCostLine(r, unmeasuredCount) {
2026
- if (r.total === 0) return null;
2027
- const parts = [
2028
- `directives ${r.directive.items} ~${r.directive.tokens}`,
2029
- `[rules ${r.items.rules} ~${r.rules} \xB7 CLAUDE.md ${r.items.claudeMd} ~${r.projectClaudeMd}]`,
2030
- `triggers ${r.firing.items} ~${r.firing.tokens}`,
2031
- `[skills ${r.items.skills} ~${r.skillDescriptors} \xB7 agents ${r.items.agents} ~${r.agentDescriptors}]`
2032
- ].join(" \xB7 ");
2033
- const external = unmeasuredCount > 0 ? ` \xB7 ${unmeasuredCount} external asset${unmeasuredCount === 1 ? "" : "s"} unmeasured` : "";
2034
- return `session-start context cost: ${r.items.total} items resident \xB7 ~${r.total} tokens/session (${parts}${external})`;
2035
- }
2036
- var RESIDENT_ROWS = [
2037
- {
2038
- // ADR-083 — 축 소계를 **행으로** 낸다. 소계가 없으면 읽는 사람이 네 줄을 머리로 더해야
2039
- // 하고, 그러면 "무엇을 줄여야 하나"가 표에서 안 보인다.
2040
- label: "\uC9C0\uC2DC\uBB38",
2041
- gap: 13,
2042
- count: (r) => r.directive.items,
2043
- tokens: (r) => r.directive.tokens
2044
- },
2045
- { label: " rules", gap: 12, count: (r) => r.items.rules, tokens: (r) => r.rules },
2046
- {
2047
- // 한 행에 두 파일(앵커 + 스캐폴드). 개수 열이 2 를 뱉어 그 사실을 드러낸다 — 라벨로
2048
- // 하나만 이름 붙이면 v26.140.0 이전과 같은 거짓 라벨이 된다.
2049
- label: " CLAUDE.md",
2050
- gap: 8,
2051
- count: (r) => r.items.claudeMd,
2052
- tokens: (r) => r.projectClaudeMd
2053
- },
2054
- {
2055
- label: "\uBC1C\uD654 \uD45C\uBA74",
2056
- gap: 10,
2057
- count: (r) => r.firing.items,
2058
- tokens: (r) => r.firing.tokens
2059
- },
2060
- {
2061
- label: " skill descriptors",
2062
- gap: 0,
2063
- count: (r) => r.items.skills,
2064
- tokens: (r) => r.skillDescriptors
2065
- },
2066
- {
2067
- label: " agent descriptors",
2068
- gap: 0,
2069
- count: (r) => r.items.agents,
2070
- tokens: (r) => r.agentDescriptors
2071
- }
2072
- ];
2073
- function formatResidentCostBlock(r) {
2074
- const n = (v) => String(v).padStart(3);
2075
- return [
2076
- " \uAC1C\uC218 \uD1A0\uD070",
2077
- ...RESIDENT_ROWS.map(
2078
- (row) => ` ${row.label}${" ".repeat(row.gap)}${n(row.count(r))}\uAC1C ~${row.tokens(r)}`
2079
- ),
2080
- " \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",
2081
- ` \uC0C1\uC8FC \uD569\uACC4 ${n(r.items.total)}\uAC1C \uC0C1\uC8FC \xB7 ~${r.total} tokens/\uC138\uC158`,
2082
- "",
2083
- " \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",
2084
- " '\uBD88\uD544\uC694\uD55C \uC9C0\uC2DC\uBB38\uC774 \uC788\uB098'\uB85C \uD55C\uB2E4. ratchet \uC740 \uC870\uC6A9\uD55C \uC99D\uAC00\uB9CC \uB9C9\uB294\uB2E4.",
2085
- " \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."
2086
- ];
2087
- }
2088
-
2089
2103
  export {
2090
2104
  __commonJS,
2091
2105
  __toESM,
@@ -2099,6 +2113,7 @@ export {
2099
2113
  isInstallScope,
2100
2114
  resolveScope,
2101
2115
  DEFAULT_OPTIONS,
2116
+ UPDATE_GROUPS,
2102
2117
  EXTERNAL_ASSETS,
2103
2118
  DEV_METHOD_SKILL_IDS,
2104
2119
  INTERNAL_BUNDLED_SKILL_IDS,
@@ -2112,6 +2127,21 @@ export {
2112
2127
  assetCliSupport,
2113
2128
  assetReachesCli,
2114
2129
  filterApplicableAssets,
2130
+ HARNESS_ANCHOR_FILE,
2131
+ HARNESS_IMPORT_LINE,
2132
+ withContinuousSkillsNote,
2133
+ renderFillScaffold,
2134
+ upsertHarnessImport,
2135
+ stripHarnessImport,
2136
+ estimateTokens,
2137
+ resolveBundleRoot,
2138
+ extractFrontmatter,
2139
+ assetCostRows,
2140
+ landsOnDisk,
2141
+ residentCost,
2142
+ summarizeContextCost,
2143
+ formatResidentCostLine,
2144
+ formatResidentCostBlock,
2115
2145
  ensureDir,
2116
2146
  copyFile,
2117
2147
  copyDir,
@@ -2133,12 +2163,6 @@ export {
2133
2163
  readInstallLog,
2134
2164
  installLogPath,
2135
2165
  legacyInstallLogPath,
2136
- HARNESS_ANCHOR_FILE,
2137
- HARNESS_IMPORT_LINE,
2138
- withContinuousSkillsNote,
2139
- renderFillScaffold,
2140
- upsertHarnessImport,
2141
- stripHarnessImport,
2142
2166
  buildAssetSpec,
2143
2167
  resolveRules,
2144
2168
  ALL_RULES,
@@ -2148,13 +2172,6 @@ export {
2148
2172
  isCliNeutralTarget,
2149
2173
  buildManifest,
2150
2174
  CATEGORIES,
2151
- CATEGORY_TITLES,
2152
- estimateTokens,
2153
- resolveBundleRoot,
2154
- assetCostRows,
2155
- residentCost,
2156
- summarizeContextCost,
2157
- formatResidentCostLine,
2158
- formatResidentCostBlock
2175
+ CATEGORY_TITLES
2159
2176
  };
2160
- //# sourceMappingURL=chunk-OYNSK6QF.js.map
2177
+ //# sourceMappingURL=chunk-242TVGMZ.js.map