@uzysjung/agent-harness 26.170.0 → 26.172.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -48,10 +48,7 @@ import {
48
48
  claimBackupDir,
49
49
  claudeRecorded,
50
50
  claudeTargets,
51
- collectPolicyHashes,
52
- collectSkillHashes,
53
51
  continuousNoteEnd,
54
- copyDir,
55
52
  copyFile,
56
53
  corruptedInstallLogMessage,
57
54
  createOutsideGuard,
@@ -71,7 +68,6 @@ import {
71
68
  isAssetSelected,
72
69
  isCliBase,
73
70
  isCliNeutralTarget,
74
- isHarnessOwned,
75
71
  isInstallScope,
76
72
  isKeyId,
77
73
  isOutsideProject,
@@ -105,7 +101,7 @@ import {
105
101
  targetsInclude,
106
102
  upsertHarnessImport,
107
103
  writeInstallLog
108
- } from "./chunk-MGFWOWF4.js";
104
+ } from "./chunk-T2HBTG32.js";
109
105
 
110
106
  // node_modules/sisteransi/src/index.js
111
107
  var require_src = __commonJS({
@@ -1200,6 +1196,13 @@ var FILES = `
1200
1196
  .claude/skills/verification-loop/SKILL.md 1744f867f7868b83b480341c2e8d08bff3848c2408255871d3aba0c2f7eff2f2
1201
1197
  .claude/skills/verification-loop/agents/openai.yaml 1907fd04fea5c83d15faf14acfc978de90076d4351341cf9259639aa0c3b7bd3
1202
1198
  .claude/skills/verification-loop/references/tracks.md de11792d447c2e8ea67e8367c6655bd94646b1ba3f29184c4521cae01facc6ec
1199
+ .uzys-agent-harness/check-absence.sh b9bbc7bbdc69d51eac2a4e4748ee7c137dd585b4f3ca1d9bd2404eab9694c9b4
1200
+ .uzys-agent-harness/check-absence.sh c08d3a1df4188e5f37ddf7f7b53901184d87bdbed63c5c99afbddd0ef6a0442d
1201
+ .uzys-agent-harness/check-absence.sh 3655faf030e319e26a6386b316df004f4e2fee963fe0ebacb661030e85cda92e
1202
+ .uzys-agent-harness/protect-branch.sh 3356c4941ef1d5c174b10fd8858887f00b3b06c851788064e0e3cc6a6c8c56fd
1203
+ .uzys-agent-harness/protect-branch.sh ecd46450beae58c085a0d407c34441ae470092d9963bd4a37f09416a22cfad35
1204
+ .uzys-agent-harness/spec-drift-check.sh f35a64dd9954a776c3eabf356c0cda017db30e129d08022341a07c5a9d2b6f9a
1205
+ .uzys-agent-harness/spec-drift-check.sh 3c6925b03775454ec3a81e63dda400511824a9ac6e3f8f30b302ce1edf39d062
1203
1206
  `;
1204
1207
  var LEGACY_HARNESS_FILES = (() => {
1205
1208
  const m = /* @__PURE__ */ new Map();
@@ -1535,13 +1538,14 @@ function planUpsert(args) {
1535
1538
  }
1536
1539
  function planStrip(args) {
1537
1540
  const alwaysStrip2 = args.alwaysStrip ?? (() => false);
1538
- const plan = { remove: [], kept: [], portions: /* @__PURE__ */ new Map() };
1541
+ const plan = { remove: [], removedEdited: [], kept: [], portions: /* @__PURE__ */ new Map() };
1539
1542
  for (const [key, sha] of args.recorded) {
1540
1543
  if (args.excluded.has(key)) continue;
1541
1544
  const now = args.present.get(key);
1542
1545
  if (now === void 0) continue;
1543
1546
  if (now === sha || alwaysStrip2(key)) {
1544
1547
  plan.remove.push(key);
1548
+ if (now !== sha) plan.removedEdited.push(key);
1545
1549
  } else {
1546
1550
  plan.kept.push(key);
1547
1551
  plan.portions.set(key, sha);
@@ -1827,6 +1831,7 @@ var jsonKeys = {
1827
1831
  text: changed ? serialize(root) : existing,
1828
1832
  changed,
1829
1833
  removed: plan.remove,
1834
+ removedEdited: plan.removedEdited,
1830
1835
  kept: plan.kept,
1831
1836
  portions: plan.portions,
1832
1837
  empty: Object.keys(root).length === 0
@@ -2018,6 +2023,7 @@ var lines = {
2018
2023
  text,
2019
2024
  changed: text !== existing,
2020
2025
  removed: plan.remove,
2026
+ removedEdited: plan.removedEdited,
2021
2027
  kept: plan.kept,
2022
2028
  portions: plan.portions,
2023
2029
  empty: text.trim() === ""
@@ -2472,6 +2478,7 @@ var markerMd = {
2472
2478
  text,
2473
2479
  changed: text !== existing,
2474
2480
  removed: plan.remove,
2481
+ removedEdited: plan.removedEdited,
2475
2482
  kept: plan.kept,
2476
2483
  portions: plan.portions,
2477
2484
  empty: text.trim() === ""
@@ -3508,6 +3515,7 @@ var tomlRegion = {
3508
3515
  text,
3509
3516
  changed: text !== existing,
3510
3517
  removed: plan.remove,
3518
+ removedEdited: plan.removedEdited,
3511
3519
  kept: plan.kept,
3512
3520
  portions: plan.portions,
3513
3521
  empty: text.trim() === ""
@@ -3840,7 +3848,7 @@ function harnessWrite(input) {
3840
3848
  const update = input.run === "update";
3841
3849
  if (input.excluded) return j("leave", "", "keep");
3842
3850
  if (disk === null) {
3843
- if (rec.state === "none") return j("create", update ? LINES.newInRelease : LINES.wrote, "sha");
3851
+ if (rec.state !== "sha") return j("create", update ? LINES.newInRelease : LINES.wrote, "sha");
3844
3852
  return j("create", update ? LINES.restored : LINES.wrote, "sha");
3845
3853
  }
3846
3854
  if (rec.state === "none") {
@@ -4182,6 +4190,9 @@ function ownersOf(path) {
4182
4190
  )
4183
4191
  );
4184
4192
  }
4193
+ function bundlePath(path) {
4194
+ return path.startsWith(".agents/skills/") ? `${SKILLS_DIR}${path.slice(".agents/skills/".length)}` : path;
4195
+ }
4185
4196
  function isRecordedTrackTarget(log, path) {
4186
4197
  return recordedTrackTargets(log)(path);
4187
4198
  }
@@ -4260,7 +4271,11 @@ function scanFieldOf(log, path) {
4260
4271
  ({ dir, ext }) => path.startsWith(`${CLAUDE_DIR}${dir}/`) && path.endsWith(ext)
4261
4272
  );
4262
4273
  if (policy === void 0) return "outside";
4263
- return present2((log.policyFiles ?? []).some((f) => f.path.startsWith(`${policy.dir}/`)));
4274
+ return present2(
4275
+ (log.policyFiles ?? []).some(
4276
+ (f) => f.path.startsWith(`${policy.dir}/`) && !RETIRED_PATHS.includes(f.path)
4277
+ )
4278
+ );
4264
4279
  }
4265
4280
  function isExternalOnly(path) {
4266
4281
  const owners = ownersOf(path);
@@ -4296,6 +4311,7 @@ function createInstallWriter(args) {
4296
4311
  const portionPaths = [];
4297
4312
  const backups = [];
4298
4313
  const judged = [];
4314
+ const writes = [];
4299
4315
  const shared2 = [];
4300
4316
  const outside = createOutsideGuard(projectDir);
4301
4317
  const rel = (abs) => relative(projectDir, abs).split(sep).join("/");
@@ -4310,12 +4326,15 @@ function createInstallWriter(args) {
4310
4326
  if (outside.skip(abs)) return null;
4311
4327
  const next = "source" in from ? readFileSync4(from.source, "utf8") : from.content;
4312
4328
  const disk = existsSync6(abs) ? readFileSync4(abs, "utf8") : null;
4329
+ const rec = recorded(previousLog, path, opts);
4330
+ const diskSha = rec.state === "no-sha" && disk !== null ? hashContent(disk) : null;
4313
4331
  const j4 = judge({
4314
4332
  op: "write",
4315
4333
  kind: "harness",
4316
- rec: recorded(previousLog, path, opts),
4334
+ rec: diskSha !== null && isLegacyHarnessFile(path, diskSha) ? { state: "sha", sha256: diskSha } : rec,
4317
4335
  disk,
4318
- next
4336
+ next,
4337
+ ...args.run === void 0 ? {} : { run: args.run }
4319
4338
  });
4320
4339
  let backupAbs;
4321
4340
  const write = () => {
@@ -4357,13 +4376,11 @@ function createInstallWriter(args) {
4357
4376
  default:
4358
4377
  throw new Error(`install: '${j4.record}' is not a write record (${path})`);
4359
4378
  }
4379
+ const line = backup === void 0 ? j4.line : j4.line.replace(BACKUP_PLACEHOLDER, backup);
4380
+ const backupPair = backup === void 0 || backupAbs === void 0 ? {} : { backup, backupAbs };
4381
+ writes.push({ path, verdict: j4.verdict, record: j4.record, line, ...backupPair });
4360
4382
  if (j4.line === "" || j4.verdict === "create" || j4.verdict === "overwrite") return null;
4361
- const out = {
4362
- path,
4363
- verdict: j4.verdict,
4364
- line: backup === void 0 ? j4.line : j4.line.replace(BACKUP_PLACEHOLDER, backup),
4365
- ...backup === void 0 || backupAbs === void 0 ? {} : { backup, backupAbs }
4366
- };
4383
+ const out = { path, verdict: j4.verdict, line, ...backupPair };
4367
4384
  judged.push(out);
4368
4385
  return out;
4369
4386
  }
@@ -4424,7 +4441,12 @@ function createInstallWriter(args) {
4424
4441
  if (disk === null && ![...res.portions.keys()].some((k2) => !isContainerKey(k2))) {
4425
4442
  return result("leave", "");
4426
4443
  }
4444
+ let backupAbs;
4427
4445
  if (res.changed) {
4446
+ if (disk !== null && res.removedEdited.length > 0) {
4447
+ backupAbs = backupFile(abs, now);
4448
+ backups.push(backupAbs);
4449
+ }
4428
4450
  mkdirSync(dirname(abs), { recursive: true });
4429
4451
  writeFileSync3(abs, res.text);
4430
4452
  }
@@ -4446,7 +4468,7 @@ function createInstallWriter(args) {
4446
4468
  const ids = (keys) => [...keys].flatMap((k2) => keyId(path, k2) ?? []);
4447
4469
  const out = excludedKeys(path, excluded);
4448
4470
  const removedOut = res.removed.filter((k2) => out.has(k2));
4449
- const removedEdited = res.removedEdited.filter((k2) => out.has(k2));
4471
+ const removedEdited = res.removedEdited;
4450
4472
  const keptOut = res.kept.filter((k2) => out.has(k2));
4451
4473
  const onlyRemoved = disk !== null && res.removed.length > 0 && res.replaced.length === 0 && addedKeys.length === 0 && restoredKeys.size === 0;
4452
4474
  const wrote = onlyRemoved ? "removed the harness part" : j4.line;
@@ -4454,6 +4476,7 @@ function createInstallWriter(args) {
4454
4476
  const retired = names(res.removed.filter((k2) => !out.has(k2) && !render.has(k2)));
4455
4477
  const replaced = names(res.replaced);
4456
4478
  return result(j4.verdict, line, {
4479
+ ...backupAbs === void 0 ? {} : { backup: rel(backupAbs), backupAbs },
4457
4480
  ...retired.length > 0 ? { retired } : {},
4458
4481
  ...replaced.length > 0 ? { replaced } : {},
4459
4482
  changed: res.changed,
@@ -4481,6 +4504,7 @@ function createInstallWriter(args) {
4481
4504
  portionPaths: [...portionPaths],
4482
4505
  backups: [...backups],
4483
4506
  judged: [...judged],
4507
+ writes: [...writes],
4484
4508
  shared: [...shared2],
4485
4509
  outside: outside.list()
4486
4510
  })
@@ -4769,8 +4793,8 @@ function legacyGitignoreSeed(text, render, previous) {
4769
4793
 
4770
4794
  // src/installer.ts
4771
4795
  init_esm_shims();
4772
- import { chmodSync as chmodSync3, existsSync as existsSync22, readdirSync as readdirSync5, readFileSync as readFileSync22, writeFileSync as writeFileSync8 } from "fs";
4773
- import { basename as basename4, join as join31, relative as relative4, resolve, sep as sep5 } from "path";
4796
+ import { chmodSync as chmodSync3, existsSync as existsSync22, readdirSync as readdirSync5, readFileSync as readFileSync23, writeFileSync as writeFileSync8 } from "fs";
4797
+ import { basename as basename4, join as join31, relative as relative5, resolve, sep as sep6 } from "path";
4774
4798
 
4775
4799
  // src/anchor-seed.ts
4776
4800
  init_esm_shims();
@@ -5049,41 +5073,34 @@ function createOwnedWriter(projectDir, baseline, options = {}) {
5049
5073
  return false;
5050
5074
  }
5051
5075
  if (outside.skip(absPath)) return false;
5052
- const digest = hashContent(content);
5053
- if (!existsSync9(absPath)) {
5054
- const recordedPath = baseline.has(rel);
5055
- if (refreshOnly && !opts?.createInRefresh && !recordedPath) return false;
5076
+ const recordedSha = baseline.get(rel);
5077
+ const rec = recordedSha !== void 0 ? { state: "sha", sha256: recordedSha } : { state: owned(rel) ? "no-sha" : "none" };
5078
+ const disk = existsSync9(absPath) ? readFileSync6(absPath, "utf8") : null;
5079
+ if (disk === null && refreshOnly && !opts?.createInRefresh && rec.state !== "sha")
5080
+ return false;
5081
+ const { verdict, record } = judge({ op: "write", kind: "harness", rec, disk, next: content });
5082
+ let backupRel = null;
5083
+ if (verdict === "backup+overwrite" || verdict === "overwrite" && opts?.backupFirst === true) {
5084
+ const backup = backupFile(absPath, now);
5085
+ backupPaths.push(backup);
5086
+ journal?.backup(backup);
5087
+ backupRel = relative2(projectDir, backup).split(sep3).join("/");
5088
+ if (verdict === "backup+overwrite" && rec.state !== "none") backedUp.push(rel);
5089
+ }
5090
+ if (record === "displaced" || record === "displaced+sha") {
5091
+ displace({ path: rel, change: "displaced", notes: backupRel === null ? [] : [backupRel] });
5092
+ }
5093
+ if (verdict !== "leave") {
5056
5094
  mkdirSync2(dirname2(absPath), { recursive: true });
5057
5095
  writeFileSync4(absPath, content);
5058
5096
  updated++;
5059
- if (refreshOnly && recordedPath) restored.push(rel);
5060
- } else {
5061
- const current = readFileSync6(absPath, "utf8");
5062
- const firstContact = !baseline.has(rel) && !owned(rel);
5063
- if (current === content && firstContact) {
5064
- displace({ path: rel, change: "displaced", notes: [] });
5065
- return true;
5066
- }
5067
- if (current !== content) {
5068
- if (!isHarnessOwned(baseline, rel, current)) {
5069
- const backup = backupFile(absPath, now);
5070
- backupPaths.push(backup);
5071
- journal?.backup(backup);
5072
- if (firstContact) {
5073
- const backupRel = relative2(projectDir, backup).split(sep3).join("/");
5074
- displace({ path: rel, change: "displaced", notes: [backupRel] });
5075
- } else backedUp.push(rel);
5076
- } else if (opts?.backupFirst === true) {
5077
- const backup = backupFile(absPath, now);
5078
- backupPaths.push(backup);
5079
- journal?.backup(backup);
5080
- }
5081
- writeFileSync4(absPath, content);
5082
- updated++;
5083
- }
5097
+ if (refreshOnly && verdict === "create" && rec.state === "sha") restored.push(rel);
5098
+ }
5099
+ if (record === "sha" || record === "displaced+sha") {
5100
+ const digest = hashContent(content);
5101
+ written.set(rel, digest);
5102
+ journal?.file({ path: rel, sha256: digest });
5084
5103
  }
5085
- written.set(rel, digest);
5086
- journal?.file({ path: rel, sha256: digest });
5087
5104
  return true;
5088
5105
  },
5089
5106
  ...journal === void 0 ? {} : { journal },
@@ -5158,7 +5175,7 @@ function renderRulesBlock(rules) {
5158
5175
  // src/shared-write.ts
5159
5176
  init_esm_shims();
5160
5177
  import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
5161
- import { dirname as dirname3, join as join13 } from "path";
5178
+ import { dirname as dirname3, join as join13, relative as relative3, sep as sep4 } from "path";
5162
5179
  function toPortions(path, portions) {
5163
5180
  const adapter = adapterFor(path);
5164
5181
  if (adapter === null) throw new Error(`shared-write: ${path} is not a shared file`);
@@ -5396,6 +5413,10 @@ function stripShared(params) {
5396
5413
  });
5397
5414
  }
5398
5415
  const fileRemoved = params.removeIfEmpty === true && res.removed.length > 0 && res.empty;
5416
+ const edited = res.removedEdited.length === 0 ? void 0 : {
5417
+ keys: res.removedEdited,
5418
+ backup: params.write ? relative3(projectDir, backupFile(abs)).split(sep4).join("/") : `${path}.backup-<time>`
5419
+ };
5399
5420
  if (params.write && fileRemoved) rmSync(abs);
5400
5421
  else if (params.write && res.changed) writeFileSync5(abs, res.text);
5401
5422
  const action = res.removed.length > 0 ? "removed" : res.kept.length > 0 ? "left" : "none";
@@ -5403,6 +5424,7 @@ function stripShared(params) {
5403
5424
  removed: res.removed,
5404
5425
  kept: res.kept,
5405
5426
  portions: toPortions(path, res.portions),
5427
+ ...edited === void 0 ? {} : { edited },
5406
5428
  ...fileRemoved ? { fileRemoved } : {}
5407
5429
  });
5408
5430
  }
@@ -6362,7 +6384,7 @@ function runCliTransforms(params) {
6362
6384
 
6363
6385
  // src/displaced.ts
6364
6386
  init_esm_shims();
6365
- import { lstatSync as lstatSync3, mkdirSync as mkdirSync5, renameSync } from "fs";
6387
+ import { lstatSync as lstatSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync17, renameSync } from "fs";
6366
6388
  import { dirname as dirname5, join as join23 } from "path";
6367
6389
  function exists(abs) {
6368
6390
  try {
@@ -6372,13 +6394,23 @@ function exists(abs) {
6372
6394
  return false;
6373
6395
  }
6374
6396
  }
6397
+ function isLegacyDisplaced(log, projectDir, f) {
6398
+ if (f.change !== "displaced") return false;
6399
+ let original;
6400
+ try {
6401
+ original = readFileSync17(join23(projectDir, f.notes[0] ?? f.path), "utf8");
6402
+ } catch {
6403
+ return false;
6404
+ }
6405
+ return isLegacyHarnessFile(f.path, hashContent(original)) && !isRecordedTrackTarget(log, bundlePath(f.path));
6406
+ }
6375
6407
  function restoreDisplaced(log, projectDir, opts) {
6376
6408
  const out = [];
6377
6409
  for (const f of log.rootFiles ?? []) {
6378
6410
  if (f.change !== "displaced" || !opts.has(f.path)) continue;
6379
6411
  const slot = join23(projectDir, f.path);
6380
6412
  if (outsideProjectTarget(projectDir, slot) !== null) continue;
6381
- const backup = f.notes[0] ?? null;
6413
+ const backup = isLegacyDisplaced(log, projectDir, f) ? null : f.notes[0] ?? null;
6382
6414
  const d2 = judgeDisplaced({
6383
6415
  backup,
6384
6416
  backupExists: backup !== null && exists(join23(projectDir, backup)),
@@ -6447,13 +6479,13 @@ import {
6447
6479
  constants as fsConstants,
6448
6480
  mkdirSync as mkdirSync6,
6449
6481
  readdirSync as readdirSync3,
6450
- readFileSync as readFileSync17,
6482
+ readFileSync as readFileSync18,
6451
6483
  renameSync as renameSync2,
6452
6484
  rmdirSync,
6453
6485
  rmSync as rmSync3
6454
6486
  } from "fs";
6455
6487
  import { homedir as homedir2 } from "os";
6456
- import { dirname as dirname6, join as join25, relative as relative3, sep as sep4 } from "path";
6488
+ import { dirname as dirname6, join as join25, relative as relative4, sep as sep5 } from "path";
6457
6489
  var SKILL_ROOTS = [".claude/skills", ".agents/skills"];
6458
6490
  function skillRootOf(cli) {
6459
6491
  return cli === "claude" ? ".claude/skills" : ".agents/skills";
@@ -6465,7 +6497,7 @@ function snapshotSkillRoots(projectDir) {
6465
6497
  for (const rel of listFilesRecursive(join25(projectDir, root))) {
6466
6498
  const path = `${root}/${rel}`;
6467
6499
  try {
6468
- out.set(path, hashContent(readFileSync17(join25(projectDir, path), "utf8")));
6500
+ out.set(path, hashContent(readFileSync18(join25(projectDir, path), "utf8")));
6469
6501
  } catch {
6470
6502
  }
6471
6503
  }
@@ -6762,7 +6794,7 @@ function slotFilesToKeep(projectDir, hold) {
6762
6794
  continue;
6763
6795
  const abs = join25(projectDir, path);
6764
6796
  try {
6765
- if (recordedSha !== void 0 && hashContent(readFileSync17(abs, "utf8")) === recordedSha)
6797
+ if (recordedSha !== void 0 && hashContent(readFileSync18(abs, "utf8")) === recordedSha)
6766
6798
  continue;
6767
6799
  accessSync(abs, fsConstants.R_OK);
6768
6800
  } catch {
@@ -6776,12 +6808,12 @@ function slotFilesToKeep(projectDir, hold) {
6776
6808
  function settleAside(projectDir, f, restore) {
6777
6809
  let now = null;
6778
6810
  try {
6779
- now = readFileSync17(f.abs);
6811
+ now = readFileSync18(f.abs);
6780
6812
  } catch (e2) {
6781
6813
  const code = e2.code;
6782
6814
  if (code !== "ENOENT" && code !== "ENOTDIR") throw e2;
6783
6815
  }
6784
- if (restore || (now === null ? !f.recorded : now.equals(readFileSync17(f.aside)))) {
6816
+ if (restore || (now === null ? !f.recorded : now.equals(readFileSync18(f.aside)))) {
6785
6817
  mkdirSync6(dirname6(f.abs), { recursive: true });
6786
6818
  renameSync2(f.aside, f.abs);
6787
6819
  return "back";
@@ -6815,7 +6847,7 @@ function pruneEmptyDirs(dir) {
6815
6847
  }
6816
6848
  }
6817
6849
  function toProjectRel(projectDir, abs) {
6818
- return relative3(projectDir, abs).split(sep4).join("/");
6850
+ return relative4(projectDir, abs).split(sep5).join("/");
6819
6851
  }
6820
6852
  function recordRefreshedFiles(projectDir, log, report, displaced, writeLog) {
6821
6853
  const next = new Map(
@@ -6916,7 +6948,7 @@ function detectVersion(method, spawn, scope, projectDir) {
6916
6948
  case "npm": {
6917
6949
  const pkgJson = scope === "global" ? detectGlobalNpmPackageJsonPath(method.pkg, spawn) : join25(projectDir, "node_modules", method.pkg, "package.json");
6918
6950
  if (!pkgJson || !existsSync19(pkgJson)) return void 0;
6919
- const parsed = JSON.parse(readFileSync17(pkgJson, "utf8"));
6951
+ const parsed = JSON.parse(readFileSync18(pkgJson, "utf8"));
6920
6952
  return parsed.version;
6921
6953
  }
6922
6954
  default:
@@ -6930,7 +6962,7 @@ function detectPluginVersion(pluginId, scope, projectDir) {
6930
6962
  const claudeScope = scope === "global" ? "user" : "project";
6931
6963
  const path = join25(homedir2(), ".claude/plugins/installed_plugins.json");
6932
6964
  if (!existsSync19(path)) return void 0;
6933
- const data = JSON.parse(readFileSync17(path, "utf8"));
6965
+ const data = JSON.parse(readFileSync18(path, "utf8"));
6934
6966
  const entries = data.plugins?.[pluginId] ?? [];
6935
6967
  const matches = entries.filter(
6936
6968
  (e2) => claudeScope === "user" ? e2.scope === "user" : e2.scope === "project" && e2.projectPath === projectDir
@@ -6962,7 +6994,7 @@ function getNpmGlobalRoot(spawn) {
6962
6994
 
6963
6995
  // src/key-ids.ts
6964
6996
  init_esm_shims();
6965
- import { readFileSync as readFileSync18 } from "fs";
6997
+ import { readFileSync as readFileSync19 } from "fs";
6966
6998
  import { join as join26 } from "path";
6967
6999
  function renderedKeyIds(harnessRoot, input, clis) {
6968
7000
  const out = /* @__PURE__ */ new Set();
@@ -6986,7 +7018,7 @@ function renderedKeyIds(harnessRoot, input, clis) {
6986
7018
  for (const name of Object.keys(mcp.mcpServers)) add(".mcp.json", `mcpServers.${name}`);
6987
7019
  for (const key of gitignoreRender().keys()) add(".gitignore", key);
6988
7020
  if (clis.includes("claude")) {
6989
- const template = readFileSync18(join26(harnessRoot, "templates/settings.json"), "utf8");
7021
+ const template = readFileSync19(join26(harnessRoot, "templates/settings.json"), "utf8");
6990
7022
  for (const key of renderSettingsPortion(template, () => true).keys()) {
6991
7023
  add(".claude/settings.json", key);
6992
7024
  }
@@ -7050,7 +7082,7 @@ function refreshLinkedSkillBodies(params) {
7050
7082
 
7051
7083
  // src/out-of-track.ts
7052
7084
  init_esm_shims();
7053
- import { lstatSync as lstatSync4, readFileSync as readFileSync19, rmSync as rmSync4 } from "fs";
7085
+ import { lstatSync as lstatSync4, readFileSync as readFileSync20, rmSync as rmSync4 } from "fs";
7054
7086
  import { join as join28 } from "path";
7055
7087
  var AGENTS_RULE = /^\.agents\/rules\/([^/]+)\.md$/;
7056
7088
  var ANCHOR_RULE = "uzys-harness";
@@ -7080,7 +7112,7 @@ function reclaimOutOfTrack(args) {
7080
7112
  out.bringBack[f.path] = tracksBringing(name);
7081
7113
  continue;
7082
7114
  }
7083
- const disk = stat === void 0 ? null : readFileSync19(abs, "utf8");
7115
+ const disk = stat === void 0 ? null : readFileSync20(abs, "utf8");
7084
7116
  const verdict = judge({
7085
7117
  op: "remove",
7086
7118
  kind: "harness",
@@ -7112,7 +7144,7 @@ function tracksBringing(rule) {
7112
7144
 
7113
7145
  // src/state.ts
7114
7146
  init_esm_shims();
7115
- import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
7147
+ import { existsSync as existsSync20, readFileSync as readFileSync21 } from "fs";
7116
7148
  import { join as join29 } from "path";
7117
7149
  var META_FILE = ".claude/.installed-tracks";
7118
7150
  var LEGACY_ANCHOR_FILE = ".claude/CLAUDE.md";
@@ -7141,7 +7173,7 @@ function tracksFromLog(tracks) {
7141
7173
  }
7142
7174
  function collectTraces(projectDir) {
7143
7175
  const at5 = (rel) => join29(projectDir, rel);
7144
- const contains = (rel, needle) => existsSync20(at5(rel)) && readFileSync20(at5(rel), "utf8").includes(needle);
7176
+ const contains = (rel, needle) => existsSync20(at5(rel)) && readFileSync21(at5(rel), "utf8").includes(needle);
7145
7177
  const traces = [];
7146
7178
  if (existsSync20(at5(META_FILE)))
7147
7179
  traces.push({ path: META_FILE, tracks: readMetafile(at5(META_FILE)) });
@@ -7156,7 +7188,7 @@ function collectTraces(projectDir) {
7156
7188
  }
7157
7189
  function readMetafile(path) {
7158
7190
  const seen = /* @__PURE__ */ new Set();
7159
- for (const word of readFileSync20(path, "utf8").split(/\s+/)) {
7191
+ for (const word of readFileSync21(path, "utf8").split(/\s+/)) {
7160
7192
  if (isTrack(word)) seen.add(word);
7161
7193
  }
7162
7194
  return [...seen].sort();
@@ -7200,10 +7232,8 @@ init_esm_shims();
7200
7232
  import {
7201
7233
  copyFileSync,
7202
7234
  existsSync as existsSync21,
7203
- lstatSync as lstatSync5,
7204
- mkdirSync as mkdirSync7,
7205
7235
  readdirSync as readdirSync4,
7206
- readFileSync as readFileSync21,
7236
+ readFileSync as readFileSync22,
7207
7237
  rmdirSync as rmdirSync2,
7208
7238
  statSync as statSync4,
7209
7239
  unlinkSync as unlinkSync2,
@@ -7268,91 +7298,12 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
7268
7298
  demotedAgents: [],
7269
7299
  mcpAllowlistRetired: null
7270
7300
  };
7271
- const fresh = installNewAssets(projectDir, templatesDir, wants, outside);
7272
- report.installedNew = fresh.installed;
7273
- report.restored = fresh.restored;
7274
7301
  const logAtStart = readInstallLog(projectDir);
7275
7302
  const claudeManaged = logAtStart === null || installedClis(logAtStart).includes("claude");
7276
- const recordOf = recordedStateOf(logAtStart);
7277
- const claudeDisplaced = [];
7278
7303
  report.claudeUnrecorded = !claudeManaged && logAtStart !== null && existsSync21(claudeDir) ? recordClaudeCommand(logAtStart, installedTracks(projectDir)) : null;
7279
- const policyBase = policyBaseline(projectDir);
7280
- const prunedPaths = /* @__PURE__ */ new Set();
7281
- for (const { dir, ext } of POLICY_DIRS) {
7282
- if (!claudeManaged || !wants(dir === "hooks" ? "hooks" : "rules")) continue;
7283
- const target = join30(claudeDir, dir);
7284
- const source = join30(templatesDir, dir);
7285
- const label = `.claude/${dir}`;
7286
- const ctx = {
7287
- prefix: dir,
7288
- baseline: policyBase,
7289
- outside,
7290
- excluded: excludedIds(logAtStart),
7291
- // 0단계가 방금 깐 파일은 기록 전이지만 하네스 것이다 — 첫 접촉으로 읽지 않는다
7292
- justWritten: new Set(fresh.installed),
7293
- ...recordOf === void 0 ? {} : { recordOf }
7294
- };
7295
- const synced = updateDir(target, source, ext, ctx);
7296
- report.updated[label] = synced.updated;
7297
- report.policyBackedUp.push(...synced.backedUp);
7298
- claudeDisplaced.push(...synced.displaced);
7299
- report.pruned[label] = pruneOrphans(target, source, ext, ctx);
7300
- for (const f of report.pruned[label]) prunedPaths.add(`${label}/${f}`);
7301
- }
7302
- const syncedDirs = POLICY_DIRS.filter((d2) => wants(d2.dir === "hooks" ? "hooks" : "rules")).map(
7303
- (d2) => d2.dir
7304
- );
7305
- if (claudeManaged && syncedDirs.length > 0)
7306
- refreshPolicyBaseline(
7307
- projectDir,
7308
- templatesDir,
7309
- syncedDirs,
7310
- outsidePaths(outside, ".claude/"),
7311
- notOursUnder(claudeDisplaced, ".claude/")
7312
- );
7313
- const prunedRestored = logAtStart === null || prunedPaths.size === 0 ? [] : restoreDisplaced(logAtStart, projectDir, { has: (p2) => prunedPaths.has(p2), write: true });
7314
- const skillTargets = logAtStart === null ? void 0 : recordedSkillIds(projectDir, logAtStart);
7315
- const skillSync = claudeManaged && wants("skills") ? syncSkills(
7316
- join30(claudeDir, "skills"),
7317
- join30(templatesDir, "skills"),
7318
- skillBaseline(projectDir),
7319
- /* @__PURE__ */ new Date(),
7320
- (relInSkills) => foreignOwnedTarget(projectDir, `.claude/skills/${relInSkills}`),
7321
- outside,
7322
- excludedSkillOf(logAtStart),
7323
- {
7324
- ...recordOf === void 0 ? {} : { recordOf },
7325
- ...skillTargets === void 0 ? {} : { targets: skillTargets }
7326
- }
7327
- ) : {
7328
- updated: 0,
7329
- backedUp: [],
7330
- skippedLinks: [],
7331
- foreignOwned: [],
7332
- pruned: [],
7333
- displaced: []
7334
- };
7335
- claudeDisplaced.push(...skillSync.displaced);
7336
- if (wants("skills")) report.updated[".claude/skills"] = skillSync.updated;
7337
- report.skillsBackedUp = skillSync.backedUp;
7338
- report.skillsSkippedLinks = skillSync.skippedLinks;
7339
- report.skillsPruned = skillSync.pruned;
7340
- const newSkills = claudeManaged && wants("new-skills") ? installNewSkillDirs(projectDir, templatesDir, installedTracks(projectDir), outside) : { installed: [], restored: [], foreignOwned: [] };
7341
- report.installedNew.push(...newSkills.installed);
7342
- report.restored.push(...newSkills.restored);
7343
- const copiedSkills = [...newSkills.installed, ...newSkills.restored];
7344
- if (claudeManaged && wants("skills"))
7345
- refreshSkillBaseline(
7346
- projectDir,
7347
- templatesDir,
7348
- outsidePaths(outside, ".claude/skills/"),
7349
- skillTargets,
7350
- notOursUnder(claudeDisplaced, ".claude/skills/")
7351
- );
7352
- else if (claudeManaged && copiedSkills.length > 0)
7353
- recordNewSkillBaseline(projectDir, templatesDir, copiedSkills);
7354
- if (claudeDisplaced.length > 0 || prunedRestored.length > 0)
7355
- recordDisplaced(projectDir, claudeDisplaced, prunedRestored);
7304
+ const harness = logAtStart === null ? { displaced: [], restored: [], foreignOwned: [], outside: [] } : updateHarnessFiles({ projectDir, templatesDir, log: logAtStart, wants, outside, report });
7305
+ const claudeDisplaced = harness.displaced;
7306
+ const prunedRestored = harness.restored;
7356
7307
  if (existsSync21(claudeDir) && claudeManaged) {
7357
7308
  if (wants("anchor")) syncHarnessAnchor(projectDir, templatesDir, report, outside);
7358
7309
  }
@@ -7388,8 +7339,6 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
7388
7339
  if (wants("hooks")) report.mcpAllowlistRetired = retireMcpAllowlist(projectDir);
7389
7340
  report.retiredAgents = staleAgentFiles(claudeDir);
7390
7341
  report.demotedAgents = demotedAgentFiles(claudeDir, installedTracks(projectDir));
7391
- if (logAtStart !== null && wants("rules"))
7392
- refreshNeutralHelpers(projectDir, templatesDir, report);
7393
7342
  const external = wants("external") ? refreshExternalCli(projectDir, harnessRoot, legacyDroppedKeys(logAtStart), harnessMcp) : {
7394
7343
  externalUpdated: 0,
7395
7344
  externalBackedUp: [],
@@ -7461,12 +7410,7 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
7461
7410
  report.restoredWithout = restoredWithoutArgs(report.restored, installedTracks(projectDir));
7462
7411
  const saidBySlotRow = new Set(report.skillsSkippedLinks.map((id) => `.claude/skills/${id}`));
7463
7412
  const merged = [];
7464
- for (const f of [
7465
- ...skillSync.foreignOwned,
7466
- ...fresh.foreignOwned,
7467
- ...newSkills.foreignOwned,
7468
- ...external.externalForeignOwned
7469
- ]) {
7413
+ for (const f of [...harness.foreignOwned, ...external.externalForeignOwned]) {
7470
7414
  if (!saidBySlotRow.has(f) && !merged.includes(f)) merged.push(f);
7471
7415
  }
7472
7416
  report.foreignOwned = merged;
@@ -7496,7 +7440,12 @@ function runUpdateMode(projectDir, templatesDir, harnessRoot, deps = {}, only) {
7496
7440
  ).filter((id) => !skillRefresh.notInCatalog.includes(id))
7497
7441
  ].filter((id) => !saidAsRemoved.has(id));
7498
7442
  report.externalSkillsUnknown = skillRefresh.unknown;
7499
- const outsideLinks = mergeOutside(outside.list(), sharedWrites.outside, external.externalOutside);
7443
+ const outsideLinks = mergeOutside(
7444
+ harness.outside,
7445
+ outside.list(),
7446
+ sharedWrites.outside,
7447
+ external.externalOutside
7448
+ );
7500
7449
  if (outsideLinks.length > 0) report.outsideLinks = outsideLinks;
7501
7450
  const displacedAll = [...claudeDisplaced, ...external.displaced];
7502
7451
  const displacedMoved = displacedAll.filter((f) => f.notes.length > 0).map((f) => f.path);
@@ -7590,7 +7539,9 @@ function checksumOnRecord(log) {
7590
7539
  ...(log.policyFiles ?? []).map((f) => `.claude/${f.path}`),
7591
7540
  ...(log.skillFiles ?? []).map((f) => `.claude/skills/${f.path}`),
7592
7541
  ...(log.externalFiles ?? []).map((f) => f.path),
7593
- ...log.assets.flatMap((a) => a.files ?? []).map((f) => f.path)
7542
+ ...log.assets.flatMap((a) => a.files ?? []).map((f) => f.path),
7543
+ // #800 — 함께 쓰는 파일은 몫마다 sha 가 있다 — 그 백업은 설치자 편집이 확실하다
7544
+ ...(log.portions ?? []).map((p2) => p2.path)
7594
7545
  ]);
7595
7546
  const anchor = log.templates.rootClaudeMd;
7596
7547
  if (anchor !== void 0) paths.add(anchor.path);
@@ -7603,7 +7554,7 @@ function retiredFeaturesPart(projectDir, f) {
7603
7554
  }
7604
7555
  function legacyHookOnDisk(projectDir, rel) {
7605
7556
  try {
7606
- return hasLegacyHarnessHook(readFileSync21(join30(projectDir, rel), "utf8"));
7557
+ return hasLegacyHarnessHook(readFileSync22(join30(projectDir, rel), "utf8"));
7607
7558
  } catch {
7608
7559
  return false;
7609
7560
  }
@@ -7635,7 +7586,7 @@ function writeUpdateSharedFiles(args) {
7635
7586
  const manifest = buildManifest(spec);
7636
7587
  writeSettingsShared(
7637
7588
  writer,
7638
- readFileSync21(settingsSource, "utf8"),
7589
+ readFileSync22(settingsSource, "utf8"),
7639
7590
  projectDir,
7640
7591
  log,
7641
7592
  (script) => {
@@ -7729,44 +7680,197 @@ function demotedAgentFiles(claudeDir, tracks) {
7729
7680
  );
7730
7681
  return readdirSync4(dir, { withFileTypes: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".md")).map((e2) => basename3(e2.name, ".md")).filter((id) => demoted.includes(id));
7731
7682
  }
7732
- function installNewAssets(projectDir, templatesDir, wants, outside) {
7733
- const installed = [];
7734
- const restored = [];
7683
+ function updateHarnessTargets(projectDir, templatesDir, log, wants, report, foreignOwned) {
7684
+ const files = [];
7685
+ const claude = installedClis(log).includes("claude");
7686
+ const excluded = excludedIds(log);
7687
+ for (const e2 of trackOnlyFileAssets(installedTracks(projectDir))) {
7688
+ if (e2.target === HARNESS_ANCHOR_FILE || e2.target === SETTINGS_TARGET) continue;
7689
+ if (!wants(e2.target.startsWith(".claude/hooks/") ? "hooks" : "rules")) continue;
7690
+ if (e2.target.startsWith(".claude/") && !claude) continue;
7691
+ if (isBaselineExcluded(e2.target, excluded)) continue;
7692
+ const source = join30(templatesDir, e2.source);
7693
+ if (!existsSync21(source)) continue;
7694
+ const policy = POLICY_DIRS.find((d2) => e2.target.startsWith(`.claude/${d2.dir}/`));
7695
+ files.push({
7696
+ path: e2.target,
7697
+ source,
7698
+ unit: e2.target,
7699
+ unitExisted: existsSync21(join30(projectDir, e2.target)),
7700
+ label: policy === void 0 ? e2.target : `.claude/${policy.dir}`
7701
+ });
7702
+ }
7703
+ const refreshedSkills = [];
7704
+ const skillsSource = join30(templatesDir, "skills");
7705
+ if (!claude || !(wants("skills") || wants("new-skills")) || !existsSync21(skillsSource)) {
7706
+ return { files, refreshedSkills };
7707
+ }
7708
+ const ids = recordedSkillIds(projectDir, log);
7709
+ const creatable = claudeSkillIds(projectDir, log);
7710
+ const excludedSkill = excludedSkillOf(log);
7711
+ for (const d2 of readdirSync4(skillsSource, { withFileTypes: true })) {
7712
+ if (!d2.isDirectory()) continue;
7713
+ const unit = `.claude/skills/${d2.name}`;
7714
+ if (occupiedByNonDirectory(join30(projectDir, unit))) {
7715
+ report.skillsSkippedLinks.push(d2.name);
7716
+ continue;
7717
+ }
7718
+ if (!ids.has(d2.name) || excludedSkill(d2.name)) continue;
7719
+ const existed = existsSync21(join30(projectDir, unit));
7720
+ if (existed ? !wants("skills") : !wants("new-skills") || !creatable.has(d2.name)) continue;
7721
+ for (const rel of listFilesRecursive(join30(skillsSource, d2.name))) {
7722
+ const foreign = foreignOwnedTarget(projectDir, `${unit}/${rel}`);
7723
+ if (foreign !== null) {
7724
+ if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
7725
+ continue;
7726
+ }
7727
+ const source = join30(skillsSource, d2.name, rel);
7728
+ files.push({
7729
+ path: `${unit}/${rel}`,
7730
+ source,
7731
+ unit,
7732
+ unitExisted: existed,
7733
+ label: ".claude/skills"
7734
+ });
7735
+ }
7736
+ if (existed) refreshedSkills.push(d2.name);
7737
+ }
7738
+ return { files, refreshedSkills };
7739
+ }
7740
+ function updateHarnessFiles(args) {
7741
+ const { projectDir, templatesDir, log, wants, outside, report } = args;
7742
+ const now = /* @__PURE__ */ new Date();
7743
+ const claude = installedClis(log).includes("claude");
7744
+ const claims = retiredClaims(log, projectDir, log.policyFiles ?? []);
7745
+ const isTarget = recordedTrackTargets(log);
7746
+ const recordOf = (path) => recorded(log, path, { isTarget });
7747
+ const writer = createInstallWriter({
7748
+ projectDir,
7749
+ previousLog: log,
7750
+ excluded: excludedIds(log),
7751
+ now,
7752
+ run: "update"
7753
+ });
7735
7754
  const foreignOwned = [];
7736
- const log = readInstallLog(projectDir);
7737
- const claudeSelected = installedClis(log).includes("claude");
7738
- const baselineExcluded = excludedIds(log);
7739
- const priorBaseline = policyBaseline(projectDir);
7740
- const wiredBefore = new Set(
7741
- (log?.portions ?? []).filter((p2) => p2.path === SETTINGS_TARGET).flatMap((p2) => p2.key.split("#")[1]?.replace(/\{\}$/, "") ?? [])
7755
+ const targets = updateHarnessTargets(projectDir, templatesDir, log, wants, report, foreignOwned);
7756
+ for (const t of targets.files) writer.harness(t.path, { source: t.source }, { isTarget });
7757
+ const prunedPolicy = /* @__PURE__ */ new Set();
7758
+ for (const { dir, ext } of POLICY_DIRS) {
7759
+ if (!claude || !wants(dir === "hooks" ? "hooks" : "rules")) continue;
7760
+ const label = `.claude/${dir}`;
7761
+ report.updated[label] = 0;
7762
+ const ctx = { prefix: dir, outside, recordOf };
7763
+ report.pruned[label] = pruneOrphans(
7764
+ join30(projectDir, label),
7765
+ join30(templatesDir, dir),
7766
+ ext,
7767
+ ctx,
7768
+ now
7769
+ );
7770
+ for (const f of report.pruned[label]) prunedPolicy.add(`${dir}/${f}`);
7771
+ }
7772
+ if (claude && wants("skills")) report.updated[".claude/skills"] = 0;
7773
+ const skillPrune = pruneSkillExtras(
7774
+ projectDir,
7775
+ templatesDir,
7776
+ targets.refreshedSkills,
7777
+ outside,
7778
+ recordOf,
7779
+ now
7742
7780
  );
7743
- for (const entry of trackOnlyFileAssets(installedTracks(projectDir))) {
7744
- const group = entry.target.startsWith(".claude/hooks/") || entry.target === ".claude/settings.json" ? "hooks" : "rules";
7745
- if (!wants(group)) continue;
7746
- if (entry.target === HARNESS_ANCHOR_FILE) continue;
7747
- if (isBaselineExcluded(entry.target, baselineExcluded)) continue;
7748
- const target = join30(projectDir, entry.target);
7749
- const foreign = foreignOwnedTarget(projectDir, entry.target);
7750
- if (foreign !== null) {
7751
- if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
7781
+ report.skillsPruned = skillPrune.pruned;
7782
+ report.skillsBackedUp.push(...skillPrune.backedUp);
7783
+ const restored = prunedPolicy.size === 0 ? [] : restoreDisplaced(log, projectDir, {
7784
+ has: (p2) => p2.startsWith(".claude/") && prunedPolicy.has(p2.slice(".claude/".length)),
7785
+ write: true
7786
+ });
7787
+ const ledger = writer.ledger();
7788
+ recordHarnessWrites(projectDir, ledger, {
7789
+ claims,
7790
+ prunedPolicy,
7791
+ prunedSkill: new Set(skillPrune.pruned),
7792
+ restored,
7793
+ walkedSkills: wants("skills")
7794
+ });
7795
+ const byPath = new Map(targets.files.map((t) => [t.path, t]));
7796
+ const created = /* @__PURE__ */ new Map();
7797
+ for (const w3 of ledger.writes) {
7798
+ const t = byPath.get(w3.path);
7799
+ if (t === void 0) continue;
7800
+ if (!t.unitExisted) {
7801
+ if (w3.verdict === "create")
7802
+ created.set(t.unit, created.get(t.unit) === true || w3.line === LINES.restored);
7752
7803
  continue;
7753
7804
  }
7754
- if (existsSync21(target)) continue;
7755
- const source = join30(templatesDir, entry.source);
7756
- if (!existsSync21(source)) continue;
7757
- if (outside.skip(target)) continue;
7758
- if (entry.target.startsWith(".claude/")) {
7759
- if (!claudeSelected) continue;
7760
- if (entry.target === SETTINGS_TARGET) continue;
7805
+ if (w3.verdict !== "leave") report.updated[t.label] = (report.updated[t.label] ?? 0) + 1;
7806
+ if (w3.verdict !== "backup+overwrite" || w3.record !== "sha") continue;
7807
+ if (w3.path.startsWith(".claude/skills/"))
7808
+ report.skillsBackedUp.push(w3.path.slice(".claude/skills/".length));
7809
+ else if (w3.path.startsWith(".claude/"))
7810
+ report.policyBackedUp.push(w3.path.slice(".claude/".length));
7811
+ }
7812
+ for (const [unit, wasRestored] of created)
7813
+ (wasRestored ? report.restored : report.installedNew).push(unit);
7814
+ return {
7815
+ displaced: ledger.rootFiles.filter((f) => f.change === "displaced"),
7816
+ restored,
7817
+ foreignOwned,
7818
+ outside: ledger.outside
7819
+ };
7820
+ }
7821
+ function pruneSkillExtras(projectDir, templatesDir, ids, outside, recordOf, now) {
7822
+ const pruned = [];
7823
+ const backedUp = [];
7824
+ for (const id of ids) {
7825
+ const targetSkill = join30(projectDir, ".claude/skills", id);
7826
+ const bundled = new Set(listFilesRecursive(join30(templatesDir, "skills", id)));
7827
+ for (const rel of listFilesRecursive(targetSkill)) {
7828
+ if (bundled.has(rel) || basename3(rel).includes(".backup-")) continue;
7829
+ if (foreignOwnedTarget(projectDir, `.claude/skills/${id}/${rel}`) !== null) continue;
7830
+ const targetFile = join30(targetSkill, rel);
7831
+ if (outside.skip(targetFile)) continue;
7832
+ const rec = recordOf(`.claude/skills/${id}/${rel}`);
7833
+ if (rec.state !== "sha" || hashContent(readFileSync22(targetFile, "utf8")) !== rec.sha256) {
7834
+ backupFile(targetFile, now);
7835
+ backedUp.push(`${id}/${rel}`);
7836
+ }
7837
+ unlinkSync2(targetFile);
7838
+ pruned.push(`${id}/${rel}`);
7839
+ for (let d2 = dirname7(targetFile); d2 !== targetSkill && readdirSync4(d2).length === 0; d2 = dirname7(d2))
7840
+ rmdirSync2(d2);
7761
7841
  }
7762
- mkdirSync7(dirname7(target), { recursive: true });
7763
- copyFileSync(source, target);
7764
- const recordedAs = entry.target.startsWith(".claude/") ? entry.target.slice(8) : entry.target;
7765
- const wired = entry.target.startsWith(".claude/hooks/") && wiredBefore.has(entry.target.slice(".claude/hooks/".length));
7766
- if (priorBaseline.has(recordedAs) || wired) restored.push(entry.target);
7767
- else installed.push(entry.target);
7768
7842
  }
7769
- return { installed, restored, foreignOwned };
7843
+ return { pruned, backedUp };
7844
+ }
7845
+ function recordHarnessWrites(projectDir, ledger, x) {
7846
+ const log = readInstallLog(projectDir);
7847
+ if (!log) return;
7848
+ const skillLedger = log.skillFiles === void 0 && !x.walkedSkills ? [] : ledger.skillFiles;
7849
+ const policy = mergeExternalFiles(log.policyFiles, ledger.policyFiles).filter(
7850
+ (f) => !x.prunedPolicy.has(f.path)
7851
+ );
7852
+ const have = new Set(policy.map((f) => f.path));
7853
+ const next = {
7854
+ ...log,
7855
+ policyFiles: [
7856
+ ...policy,
7857
+ ...x.claims.filter((c3) => !x.prunedPolicy.has(c3.path) && !have.has(c3.path))
7858
+ ],
7859
+ skillFiles: mergeExternalFiles(log.skillFiles, skillLedger).filter(
7860
+ (f) => !x.prunedSkill.has(f.path)
7861
+ ),
7862
+ externalFiles: mergeExternalFiles(log.externalFiles, ledger.externalFiles),
7863
+ rootFiles: withoutRestored(
7864
+ mergeRootFiles(log.rootFiles, withoutLostBackups(log.rootFiles, ledger.rootFiles)),
7865
+ x.restored
7866
+ )
7867
+ };
7868
+ for (const key of ["policyFiles", "skillFiles", "externalFiles", "rootFiles"])
7869
+ if (next[key]?.length === 0) delete next[key];
7870
+ try {
7871
+ writeInstallLog(projectDir, next);
7872
+ } catch {
7873
+ }
7770
7874
  }
7771
7875
  var BACKUP_SUFFIX = /\.backup-\d{8}T\d{6}(-\d+)?$/;
7772
7876
  var BACKUP_SCAN_DIRS = [
@@ -7815,51 +7919,6 @@ function writeBackupList(projectDir, backups, noChecksum = /* @__PURE__ */ new S
7815
7919
  } catch {
7816
7920
  }
7817
7921
  }
7818
- function installNewSkillDirs(projectDir, templatesDir, tracks, outside) {
7819
- const installed = [];
7820
- const restored = [];
7821
- const foreignOwned = [];
7822
- const log = readInstallLog(projectDir);
7823
- const priorIds = new Set((log?.skillFiles ?? []).map((f) => f.path.split("/")[0] ?? ""));
7824
- const excluded = excludedIds(log);
7825
- const skillExcluded = excluded;
7826
- if (log !== null && !installedClis(log).includes("claude"))
7827
- return { installed, restored, foreignOwned };
7828
- const spec = buildAssetSpec({ tracks, options: DEFAULT_OPTIONS });
7829
- for (const entry of buildManifest(spec)) {
7830
- if (entry.type !== "dir" || !entry.target.startsWith(".claude/skills/")) continue;
7831
- if (!entry.applies(spec) || isBaselineExcluded(entry.target, excluded)) continue;
7832
- if (skillExcluded.has(entry.target.slice(".claude/skills/".length))) continue;
7833
- const target = join30(projectDir, entry.target);
7834
- const foreign = foreignOwnedTarget(projectDir, entry.target);
7835
- if (foreign !== null) {
7836
- if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
7837
- continue;
7838
- }
7839
- if (existsSync21(target)) continue;
7840
- const source = join30(templatesDir, entry.source);
7841
- if (!existsSync21(source)) continue;
7842
- if (outside.skip(target)) continue;
7843
- copyDir(source, target, (rel) => foreignOwnedTarget(projectDir, `${entry.target}/${rel}`));
7844
- if (priorIds.has(entry.target.slice(".claude/skills/".length))) restored.push(entry.target);
7845
- else installed.push(entry.target);
7846
- }
7847
- return { installed, restored, foreignOwned };
7848
- }
7849
- function recordNewSkillBaseline(projectDir, templatesDir, targets) {
7850
- const log = readInstallLog(projectDir);
7851
- if (!log) return;
7852
- if (log.skillFiles === void 0) return;
7853
- const ids = new Set(targets.map((t) => t.slice(".claude/skills/".length)));
7854
- const added = collectSkillHashes(projectDir, templatesDir).filter(
7855
- (f) => ids.has(f.path.split("/")[0] ?? "")
7856
- );
7857
- const kept = (log.skillFiles ?? []).filter((f) => !ids.has(f.path.split("/")[0] ?? ""));
7858
- try {
7859
- writeInstallLog(projectDir, { ...log, skillFiles: [...kept, ...added] });
7860
- } catch {
7861
- }
7862
- }
7863
7922
  function trackOnlyFileAssets(tracks) {
7864
7923
  const base = { tracks, selectedInternalSkills: [] };
7865
7924
  const optedOut = { ...base, withTauri: false };
@@ -7878,8 +7937,8 @@ function syncHarnessAnchor(projectDir, templatesDir, report, outside, now = /* @
7878
7937
  if (outside.skip(anchor)) return;
7879
7938
  const existed = existsSync21(anchor);
7880
7939
  if (existed) {
7881
- const current = readFileSync21(anchor, "utf8");
7882
- if (current !== readFileSync21(templateMd, "utf8")) {
7940
+ const current = readFileSync22(anchor, "utf8");
7941
+ if (current !== readFileSync22(templateMd, "utf8")) {
7883
7942
  const recorded2 = readInstallLog(projectDir)?.templates.rootClaudeMd;
7884
7943
  const owned = recorded2?.path === HARNESS_ANCHOR_FILE && hashContent(current) === recorded2.sha256;
7885
7944
  if (!owned) {
@@ -7902,11 +7961,11 @@ function syncHarnessAnchor(projectDir, templatesDir, report, outside, now = /* @
7902
7961
  function upsertRootImport(projectDir, outside) {
7903
7962
  const target = join30(projectDir, "CLAUDE.md");
7904
7963
  if (outside.skip(target)) return false;
7905
- const existing = existsSync21(target) ? readFileSync21(target, "utf8") : null;
7964
+ const existing = existsSync21(target) ? readFileSync22(target, "utf8") : null;
7906
7965
  const next = upsertHarnessImport(existing, {
7907
7966
  projectName: basename3(projectDir),
7908
7967
  tracks: installedTracks(projectDir),
7909
- // ADR-085 — update 는 선택 목록을 인자로 받지 않는다. `syncSkills` 와 같은 판정을 쓴다:
7968
+ // ADR-085 — update 는 선택 목록을 인자로 받지 않는다. 앞 단계(하네스 파일 쓰기)가 지나간 디스크로 판정한다:
7910
7969
  // `.claude/skills/<id>` 가 있으면 그 스킬은 이 설치가 고른 것이다.
7911
7970
  continuousSkills: CONTINUOUS_SKILLS.map((s) => s.id).filter(
7912
7971
  (id) => existsSync21(join30(projectDir, ".claude/skills", id))
@@ -7930,7 +7989,7 @@ function recordAnchorBaseline(projectDir, anchor) {
7930
7989
  ...log.templates,
7931
7990
  rootClaudeMd: {
7932
7991
  path: HARNESS_ANCHOR_FILE,
7933
- sha256: hashContent(readFileSync21(anchor, "utf8"))
7992
+ sha256: hashContent(readFileSync22(anchor, "utf8"))
7934
7993
  }
7935
7994
  }
7936
7995
  };
@@ -7979,16 +8038,20 @@ function recordedBundledSkills(projectDir, log) {
7979
8038
  return INTERNAL_BUNDLED_SKILL_IDS.filter((id) => ids.has(id));
7980
8039
  }
7981
8040
  function recordedSkillIds(projectDir, log) {
8041
+ const ids = claudeSkillIds(projectDir, log);
8042
+ for (const f of log.externalFiles ?? []) {
8043
+ const id = SHARED_SKILL_ID.exec(f.path)?.[1];
8044
+ if (id !== void 0) ids.add(id);
8045
+ }
8046
+ return ids;
8047
+ }
8048
+ function claudeSkillIds(projectDir, log) {
7982
8049
  const isTarget = recordedTrackTargets(log);
7983
8050
  const ids = new Set(bundledSkillTargets(projectDir, log));
7984
8051
  for (const f of log.skillFiles ?? []) {
7985
8052
  const id = f.path.split("/")[0] ?? "";
7986
8053
  if (recorded(log, `.claude/skills/${f.path}`, { isTarget }).state !== "none") ids.add(id);
7987
8054
  }
7988
- for (const f of log.externalFiles ?? []) {
7989
- const id = SHARED_SKILL_ID.exec(f.path)?.[1];
7990
- if (id !== void 0) ids.add(id);
7991
- }
7992
8055
  return ids;
7993
8056
  }
7994
8057
  var SHARED_SKILL_ID = /^\.agents\/skills\/([^/]+)\//;
@@ -8073,7 +8136,7 @@ function refreshExternalCli(projectDir, harnessRoot, droppedKeys, harnessMcp) {
8073
8136
  // 기록 없음: CLI 경로로는 도달 불가 — 진입 판정(#699)이 먼저 거절. 엔진 단위 방어(빈 집합).
8074
8137
  recordedClis: log === null ? [] : installedClis(log),
8075
8138
  // #761 B-1 — 외부 기록이 아예 없던 옛 판의 산출물은 첫 접촉이 아니라 체크섬 없는 하네스 파일이다(install 과 같은 판정).
8076
- // 앞 단계(`refreshNeutralHelpers`)가 헬퍼만 든 `externalFiles` 를 적어도 같은 답이다 — 헬퍼는 외부 자리로 세지 않는다(B-2)
8139
+ // 앞 단계(하네스 파일 쓰기)가 헬퍼만 든 `externalFiles` 를 적어도 같은 답이다 — 헬퍼는 외부 자리로 세지 않는다(B-2)
8077
8140
  owned: recordedOwner(log)
8078
8141
  });
8079
8142
  const anchorPath = result.anchorRetired?.action === "removed" ? result.anchorRetired.path : null;
@@ -8200,229 +8263,27 @@ function refreshExternalCli(projectDir, harnessRoot, droppedKeys, harnessMcp) {
8200
8263
  })
8201
8264
  };
8202
8265
  }
8203
- function updateDir(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
8204
- if (!existsSync21(target) || !existsSync21(source))
8205
- return { updated: 0, backedUp: [], displaced: [] };
8206
- let updated = 0;
8207
- const backedUp = [];
8208
- const displaced = [];
8209
- for (const file of readdirSync4(target)) {
8210
- if (!file.endsWith(ext)) continue;
8211
- const targetFile = join30(target, file);
8212
- const sourceFile = join30(source, file);
8213
- if (!existsSync21(sourceFile)) continue;
8214
- if (ctx.outside?.skip(targetFile)) continue;
8215
- const projectRel = `.claude/${ctx.prefix}/${file}`;
8216
- if (ctx.excluded && isBaselineExcluded(projectRel, ctx.excluded)) continue;
8217
- if (ctx.justWritten?.has(projectRel)) continue;
8218
- const next = readFileSync21(sourceFile, "utf8");
8219
- const rec = ctx.recordOf?.(projectRel) ?? baselineRecord(ctx.baseline, `${ctx.prefix}/${file}`);
8220
- const w3 = judgedUpdateWrite(targetFile, rec, next, now);
8221
- if (w3.changed) updated++;
8222
- if (w3.kind === "edited") backedUp.push(`${ctx.prefix}/${file}`);
8223
- const d2 = displacedEntry(w3, projectRel, targetFile);
8224
- if (d2 !== null) displaced.push(d2);
8225
- }
8226
- return { updated, backedUp, displaced };
8227
- }
8228
- function syncSkills(targetDir, sourceDir, baseline, now = /* @__PURE__ */ new Date(), foreignOf, outside, excludedSkill, opts = {}) {
8229
- if (!existsSync21(targetDir) || !existsSync21(sourceDir))
8230
- return {
8231
- updated: 0,
8232
- backedUp: [],
8233
- skippedLinks: [],
8234
- foreignOwned: [],
8235
- pruned: [],
8236
- displaced: []
8237
- };
8238
- let updated = 0;
8239
- const backedUp = [];
8240
- const skippedLinks = [];
8241
- const foreignOwned = [];
8242
- const pruned = [];
8243
- const displaced = [];
8244
- for (const skill of readdirSync4(sourceDir, { withFileTypes: true })) {
8245
- if (!skill.isDirectory()) continue;
8246
- const targetSkill = join30(targetDir, skill.name);
8247
- if (occupiedByNonDirectory(targetSkill)) {
8248
- skippedLinks.push(skill.name);
8249
- continue;
8250
- }
8251
- if (opts.targets !== void 0 && !opts.targets.has(skill.name)) continue;
8252
- if (!existsSync21(targetSkill)) continue;
8253
- if (excludedSkill?.(skill.name)) continue;
8254
- for (const rel of listFilesRecursive(join30(sourceDir, skill.name))) {
8255
- const targetFile = join30(targetSkill, rel);
8256
- const foreign = foreignOf(`${skill.name}/${rel}`);
8257
- if (foreign !== null) {
8258
- if (!foreignOwned.includes(foreign)) foreignOwned.push(foreign);
8259
- continue;
8260
- }
8261
- if (outside?.skip(targetFile)) continue;
8262
- const next = readFileSync21(join30(sourceDir, skill.name, rel), "utf8");
8263
- const projectRel = `.claude/skills/${skill.name}/${rel}`;
8264
- const rec = opts.recordOf?.(projectRel) ?? baselineRecord(baseline, `${skill.name}/${rel}`);
8265
- const w3 = judgedUpdateWrite(targetFile, rec, next, now);
8266
- if (w3.changed) updated++;
8267
- if (w3.kind === "edited") backedUp.push(`${skill.name}/${rel}`);
8268
- const d2 = displacedEntry(w3, projectRel, targetFile);
8269
- if (d2 !== null) displaced.push(d2);
8270
- }
8271
- const bundled = new Set(listFilesRecursive(join30(sourceDir, skill.name)));
8272
- for (const rel of listFilesRecursive(targetSkill)) {
8273
- if (bundled.has(rel) || basename3(rel).includes(".backup-")) continue;
8274
- if (foreignOf(`${skill.name}/${rel}`) !== null) continue;
8275
- const targetFile = join30(targetSkill, rel);
8276
- if (outside?.skip(targetFile)) continue;
8277
- const rec = opts.recordOf?.(`.claude/skills/${skill.name}/${rel}`) ?? baselineRecord(baseline, `${skill.name}/${rel}`);
8278
- if (rec.state !== "sha" || hashContent(readFileSync21(targetFile, "utf8")) !== rec.sha256) {
8279
- backupFile(targetFile, now);
8280
- backedUp.push(`${skill.name}/${rel}`);
8281
- }
8282
- unlinkSync2(targetFile);
8283
- pruned.push(`${skill.name}/${rel}`);
8284
- for (let d2 = dirname7(targetFile); d2 !== targetSkill && readdirSync4(d2).length === 0; d2 = dirname7(d2))
8285
- rmdirSync2(d2);
8286
- }
8287
- }
8288
- return { updated, backedUp, skippedLinks, foreignOwned, pruned, displaced };
8289
- }
8290
- function judgedUpdateWrite(abs, rec, next, now) {
8291
- const disk = existsSync21(abs) ? readFileSync21(abs, "utf8") : null;
8292
- const j4 = judge({ op: "write", kind: "harness", rec, disk, next, run: "update" });
8293
- let backup;
8294
- switch (j4.verdict) {
8295
- case "leave":
8296
- break;
8297
- case "create":
8298
- mkdirSync7(dirname7(abs), { recursive: true });
8299
- writeFileSync7(abs, next);
8300
- break;
8301
- case "overwrite":
8302
- writeFileSync7(abs, next);
8303
- break;
8304
- case "backup+overwrite":
8305
- backup = backupFile(abs, now);
8306
- writeFileSync7(abs, next);
8307
- break;
8308
- default:
8309
- throw new Error(`update: '${j4.verdict}' is not a write verdict (${abs})`);
8310
- }
8311
- const changed = j4.verdict !== "leave";
8312
- if (j4.record === "displaced") return { kind: "displacedSame", changed };
8313
- if (backup === void 0) return { kind: changed ? "written" : "same", changed };
8314
- return { kind: j4.record === "displaced+sha" ? "displaced" : "edited", changed, backup };
8315
- }
8316
- function displacedEntry(w3, projectRel, abs) {
8317
- if (w3.kind === "displacedSame") return { path: projectRel, change: "displaced", notes: [] };
8318
- if (w3.kind !== "displaced" || w3.backup === void 0) return null;
8319
- const backupRel = `${projectRel}${w3.backup.slice(abs.length)}`;
8320
- return { path: projectRel, change: "displaced", notes: [backupRel] };
8321
- }
8322
- function baselineRecord(baseline, key) {
8323
- const sha256 = baseline.get(key);
8324
- return sha256 === void 0 ? { state: "no-sha" } : { state: "sha", sha256 };
8325
- }
8326
- function recordedStateOf(log) {
8327
- if (log === null) return void 0;
8328
- const isTarget = recordedTrackTargets(log);
8329
- return (projectRel) => recorded(log, projectRel, { isTarget });
8330
- }
8331
- function notOursUnder(displaced, prefix) {
8332
- return new Set(
8333
- displaced.filter((f) => f.notes.length === 0 && f.path.startsWith(prefix)).map((f) => f.path.slice(prefix.length))
8334
- );
8335
- }
8336
8266
  function withoutLostBackups(prior, add) {
8337
8267
  const backedUp = new Set(
8338
8268
  (prior ?? []).filter((f) => f.change === "displaced" && f.notes.length > 0).map((f) => f.path)
8339
8269
  );
8340
8270
  return add.filter((f) => f.notes.length > 0 || !backedUp.has(f.path));
8341
8271
  }
8342
- function recordDisplaced(projectDir, displaced, restored = []) {
8343
- const log = readInstallLog(projectDir);
8344
- if (!log) return;
8345
- const add = withoutLostBackups(log.rootFiles, displaced);
8346
- const settles = restored.some((d2) => d2.forget);
8347
- if (add.length === 0 && !settles) return;
8348
- const merged = add.length === 0 ? [...log.rootFiles ?? []] : mergeRootFiles(log.rootFiles, add);
8349
- try {
8350
- writeInstallLog(projectDir, { ...log, rootFiles: withoutRestored(merged, restored) });
8351
- } catch {
8352
- }
8353
- }
8354
- function skillBaseline(projectDir) {
8355
- const log = readInstallLog(projectDir);
8356
- return new Map((log?.skillFiles ?? []).map((f) => [f.path, f.sha256]));
8357
- }
8358
- function policyBaseline(projectDir) {
8359
- const log = readInstallLog(projectDir);
8360
- return new Map((log?.policyFiles ?? []).map((f) => [f.path, f.sha256]));
8361
- }
8362
- function refreshPolicyBaseline(projectDir, templatesDir, dirs, outside, notOurs = /* @__PURE__ */ new Set()) {
8363
- const log = readInstallLog(projectDir);
8364
- if (!log) return;
8365
- const excluded = excludedIds(log);
8366
- const inDirs = (path) => (dirs === void 0 || dirs.some((d2) => path.startsWith(`${d2}/`))) && !outside.has(path) && !isBaselineExcluded(`.claude/${path}`, excluded);
8367
- const rewritten = [
8368
- ...(log.policyFiles ?? []).filter((f) => !inDirs(f.path)),
8369
- ...collectPolicyHashes(projectDir, templatesDir).filter(
8370
- (f) => inDirs(f.path) && !notOurs.has(f.path)
8371
- )
8372
- ];
8373
- const policyFiles = [...rewritten, ...retiredClaims(log, projectDir, rewritten)];
8374
- const next = { ...log };
8375
- if (policyFiles.length > 0) next.policyFiles = policyFiles;
8376
- else delete next.policyFiles;
8377
- try {
8378
- writeInstallLog(projectDir, next);
8379
- } catch {
8380
- }
8381
- }
8382
- function outsidePaths(outside, prefix) {
8383
- return new Set(
8384
- outside.list().filter((o) => o.path.startsWith(prefix)).map((o) => o.path.slice(prefix.length))
8385
- );
8386
- }
8387
- function refreshSkillBaseline(projectDir, templatesDir, outside, targets, notOurs = /* @__PURE__ */ new Set()) {
8388
- const log = readInstallLog(projectDir);
8389
- if (!log) return;
8390
- const excludedSkill = excludedSkillOf(log);
8391
- const keepPrior = (path) => outside.has(path) || excludedSkill(path.split("/")[0] ?? "");
8392
- const ours = (path) => (targets === void 0 || targets.has(path.split("/")[0] ?? "")) && !notOurs.has(path);
8393
- const skillFiles = [
8394
- ...collectSkillHashes(projectDir, templatesDir).filter(
8395
- (f) => !keepPrior(f.path) && ours(f.path)
8396
- ),
8397
- ...(log.skillFiles ?? []).filter((f) => keepPrior(f.path))
8398
- ];
8399
- const next = { ...log };
8400
- if (skillFiles.length > 0) next.skillFiles = skillFiles;
8401
- else delete next.skillFiles;
8402
- try {
8403
- writeInstallLog(projectDir, next);
8404
- } catch {
8405
- }
8406
- }
8407
- function pruneOrphans(target, source, ext, ctx, now = /* @__PURE__ */ new Date()) {
8272
+ function pruneOrphans(target, source, ext, ctx, now) {
8408
8273
  if (!existsSync21(target) || !existsSync21(source)) return [];
8409
8274
  const removed = [];
8410
8275
  for (const file of readdirSync4(target)) {
8411
- if (!file.endsWith(ext)) continue;
8412
- const sourceFile = join30(source, file);
8413
- if (!existsSync21(sourceFile)) {
8414
- const targetFile = join30(target, file);
8415
- const key = `${ctx.prefix}/${file}`;
8416
- if (!ctx.baseline.has(key)) continue;
8417
- if (ctx.outside?.skip(targetFile)) continue;
8418
- try {
8419
- const current = readFileSync21(targetFile, "utf8");
8420
- const owned = ctx.baseline.get(key) === hashContent(current) && ctx.recordOf?.(`.claude/${key}`).state !== "no-sha";
8421
- if (!owned) backupFile(targetFile, now);
8422
- unlinkSync2(targetFile);
8423
- removed.push(file);
8424
- } catch {
8425
- }
8276
+ if (!file.endsWith(ext) || existsSync21(join30(source, file))) continue;
8277
+ const targetFile = join30(target, file);
8278
+ const rec = ctx.recordOf(`.claude/${ctx.prefix}/${file}`);
8279
+ try {
8280
+ const disk = readFileSync22(targetFile, "utf8");
8281
+ const { verdict } = judge({ op: "remove", kind: "harness", rec, disk, next: null });
8282
+ if (verdict === "leave" || ctx.outside.skip(targetFile)) continue;
8283
+ if (verdict === "backup+remove") backupFile(targetFile, now);
8284
+ unlinkSync2(targetFile);
8285
+ removed.push(file);
8286
+ } catch {
8426
8287
  }
8427
8288
  }
8428
8289
  return removed;
@@ -8438,53 +8299,6 @@ function retireMcpAllowlist(projectDir, now = /* @__PURE__ */ new Date()) {
8438
8299
  return null;
8439
8300
  }
8440
8301
  }
8441
- function refreshNeutralHelpers(projectDir, templatesDir, report) {
8442
- const spec = buildAssetSpec({ tracks: installedTracks(projectDir), options: DEFAULT_OPTIONS });
8443
- const refreshed = /* @__PURE__ */ new Map();
8444
- for (const entry of buildManifest(spec)) {
8445
- if (entry.type !== "file" || !isCliNeutralTarget(entry.target)) continue;
8446
- if (!entry.applies(spec)) continue;
8447
- const abs = join30(projectDir, entry.target);
8448
- const source = join30(templatesDir, entry.source);
8449
- if (!existsSync21(source)) continue;
8450
- let st2;
8451
- try {
8452
- st2 = lstatSync5(abs);
8453
- } catch {
8454
- continue;
8455
- }
8456
- if (st2.isSymbolicLink()) {
8457
- report.helpersKept = [...report.helpersKept ?? [], entry.target];
8458
- continue;
8459
- }
8460
- if (!st2.isFile()) continue;
8461
- const log2 = readInstallLog(projectDir);
8462
- const recorded2 = (log2?.externalFiles ?? []).find((f) => f.path === entry.target);
8463
- const diskSha = hashContent(readFileSync21(abs, "utf8"));
8464
- const freshContent = readFileSync21(source, "utf8");
8465
- const freshSha = hashContent(freshContent);
8466
- if (diskSha === freshSha) {
8467
- if (recorded2?.sha256 !== freshSha) refreshed.set(entry.target, freshSha);
8468
- continue;
8469
- }
8470
- if (recorded2 === void 0 || recorded2.sha256 !== diskSha) backupFile(abs);
8471
- writeFileSync7(abs, freshContent, "utf8");
8472
- refreshed.set(entry.target, freshSha);
8473
- report.updated[entry.target] = (report.updated[entry.target] ?? 0) + 1;
8474
- }
8475
- if (refreshed.size === 0) return;
8476
- const log = readInstallLog(projectDir);
8477
- if (log === null) return;
8478
- const next = (log.externalFiles ?? []).map(
8479
- (f) => refreshed.has(f.path) ? { ...f, sha256: refreshed.get(f.path) } : f
8480
- );
8481
- for (const [path, sha256] of refreshed)
8482
- if (!next.some((f) => f.path === path)) next.push({ path, sha256 });
8483
- try {
8484
- writeInstallLog(projectDir, { ...log, externalFiles: next });
8485
- } catch {
8486
- }
8487
- }
8488
8302
  function recordClaudeCommand(log, tracks) {
8489
8303
  const trackArgs = (tracks.length > 0 ? tracks : log.spec.tracks).map((t) => `--track ${t}`);
8490
8304
  return `agent-harness install ${trackArgs.join(" ")} --cli claude --scope ${log.scope}`.replace(
@@ -8529,10 +8343,10 @@ function runInstall(ctx) {
8529
8343
  const settingsPath = join31(projectDir, ".claude", "settings.json");
8530
8344
  if (existsSync22(settingsPath)) {
8531
8345
  try {
8532
- JSON.parse(readFileSync22(settingsPath, "utf8"));
8346
+ JSON.parse(readFileSync23(settingsPath, "utf8"));
8533
8347
  } catch {
8534
8348
  throw new Error(
8535
- `${relative4(process.cwd(), settingsPath)} is not valid JSON \u2014 left untouched, so no hooks were wired. Fix or delete that file, then run the install again`
8349
+ `${relative5(process.cwd(), settingsPath)} is not valid JSON \u2014 left untouched, so no hooks were wired. Fix or delete that file, then run the install again`
8536
8350
  );
8537
8351
  }
8538
8352
  }
@@ -8848,7 +8662,7 @@ function pendingKeyExcludes(projectDir, keyExclude, touched) {
8848
8662
  if (path === null || done.has(path) || !existsSync22(join31(projectDir, path))) return [];
8849
8663
  const name = adapterFor(path);
8850
8664
  const keys = [...excludedKeys(path, [id])];
8851
- const present2 = name === null ? null : ADAPTERS[name].read(readFileSync22(join31(projectDir, path), "utf8"), keys, projectDir);
8665
+ const present2 = name === null ? null : ADAPTERS[name].read(readFileSync23(join31(projectDir, path), "utf8"), keys, projectDir);
8852
8666
  return present2 !== null && present2.size === 0 ? [] : [{ id, path }];
8853
8667
  });
8854
8668
  }
@@ -9009,7 +8823,7 @@ function installClaudeBaseline(manifestSpec, projectDir, templatesDir, baselineE
9009
8823
  function writeSettingsPortion(writer, source, projectDir, previousLog, hookInstalled) {
9010
8824
  return writeSettingsShared(
9011
8825
  writer,
9012
- readFileSync22(source, "utf8"),
8826
+ readFileSync23(source, "utf8"),
9013
8827
  projectDir,
9014
8828
  previousLog,
9015
8829
  hookInstalled
@@ -9153,7 +8967,7 @@ function recordInterruptedInstall(ctx, error, run) {
9153
8967
  const codexTrust = registeredTrust(journal.codexOptIn, ctx.projectDir);
9154
8968
  const written = writtenSoFar(ledger, cliFiles, rootFiles, stage.rootImportWritten);
9155
8969
  if (codexTrust) written.push(`${codexTrust.configPath} (Codex trust entry for this folder)`);
9156
- const projectRel = (abs) => relative4(ctx.projectDir, abs).split(sep5).join("/");
8970
+ const projectRel = (abs) => relative5(ctx.projectDir, abs).split(sep6).join("/");
9157
8971
  const writtenSet = new Set(written);
9158
8972
  const backups = [...own.backups, ...journal.backups].map((abs) => {
9159
8973
  const original = projectRel(backupOriginal(abs) ?? abs);
@@ -9295,7 +9109,7 @@ function accumulateCategory(cats, entry) {
9295
9109
  function writeRootClaudeMd(projectDir, tracks, continuousSkills, harnessRoot, writer) {
9296
9110
  const target = join31(projectDir, "CLAUDE.md");
9297
9111
  if (writer.skipOutside(target)) return null;
9298
- const existing = existsSync22(target) ? readFileSync22(target, "utf-8") : null;
9112
+ const existing = existsSync22(target) ? readFileSync23(target, "utf-8") : null;
9299
9113
  const seeded = existing === null ? seedRootClaudeProjectContext(projectDir, harnessRoot) : null;
9300
9114
  const content = upsertHarnessImport(existing, {
9301
9115
  projectName: basename4(projectDir),
@@ -9329,8 +9143,8 @@ function listHookFiles(hookDir) {
9329
9143
  // src/commands/install-render.ts
9330
9144
  init_esm_shims();
9331
9145
  import { homedir as homedir3 } from "os";
9332
- import { existsSync as existsSync24, readdirSync as readdirSync6, readFileSync as readFileSync24 } from "fs";
9333
- import { join as join33, relative as relative5 } from "path";
9146
+ import { existsSync as existsSync24, readdirSync as readdirSync6, readFileSync as readFileSync25 } from "fs";
9147
+ import { join as join33, relative as relative6 } from "path";
9334
9148
 
9335
9149
  // src/preset-recommend.ts
9336
9150
  init_esm_shims();
@@ -9366,7 +9180,7 @@ function groupAssetsByCategory(assetIds) {
9366
9180
 
9367
9181
  // src/resident-entries.ts
9368
9182
  init_esm_shims();
9369
- import { existsSync as existsSync23, readFileSync as readFileSync23 } from "fs";
9183
+ import { existsSync as existsSync23, readFileSync as readFileSync24 } from "fs";
9370
9184
  import { basename as basename5, join as join32 } from "path";
9371
9185
  function residentEntries(spec) {
9372
9186
  const skillThere = (id) => INTERNAL_BUNDLED_SKILL_IDS.includes(id) && [".claude/skills", ".agents/skills"].some((d2) => existsSync23(join32(spec.projectDir, d2, id)));
@@ -9407,7 +9221,7 @@ function plannedClaudeContext(spec) {
9407
9221
  if (!spec.cli.includes("claude")) return void 0;
9408
9222
  const root = resolveBundleRoot();
9409
9223
  const rootClaude = join32(spec.projectDir, "CLAUDE.md");
9410
- const existing = existsSync23(rootClaude) ? readFileSync23(rootClaude, "utf-8") : null;
9224
+ const existing = existsSync23(rootClaude) ? readFileSync24(rootClaude, "utf-8") : null;
9411
9225
  const seeded = existing === null ? seedRootClaudeProjectContext(spec.projectDir, root) : null;
9412
9226
  if (existing === null && seeded === null) return void 0;
9413
9227
  const manifestSpec = buildManifestSpec(spec);
@@ -9573,7 +9387,7 @@ function renderUpdateSummary(log, spec, report) {
9573
9387
  log(
9574
9388
  infoRow(
9575
9389
  "NEXT",
9576
- unmeasured === backups.length ? 'nothing to re-apply unless you remember editing one of them \u2014 then ask audit-harness-fit: "update \uBC31\uC5C5\uBCF8\uC758 \uB0B4 \uD3B8\uC9D1\uC744 \uC0C8 \uD310\uC5D0 \uB2E4\uC2DC \uC5B9\uC5B4\uC918"' : 'to re-apply your edits on the new version, ask audit-harness-fit: "update \uBC31\uC5C5\uBCF8\uC758 \uB0B4 \uD3B8\uC9D1\uC744 \uC0C8 \uD310\uC5D0 \uB2E4\uC2DC \uC5B9\uC5B4\uC918"'
9390
+ unmeasured === backups.length ? 'nothing to carry over unless you remember editing one of them \u2014 then ask audit-harness-fit: "update \uBC31\uC5C5\uBCF8\uC758 \uB0B4 \uD3B8\uC9D1\uC744 \uB0B4 \uD30C\uC77C\uB85C \uC62E\uACA8\uC918"' : 'backups are not loaded \u2014 to keep your edits across updates, ask audit-harness-fit: "update \uBC31\uC5C5\uBCF8\uC758 \uB0B4 \uD3B8\uC9D1\uC744 \uB0B4 \uD30C\uC77C\uB85C \uC62E\uACA8\uC918" (update replaces harness files every time, so edits put back inside them return as backups)'
9577
9391
  )
9578
9392
  );
9579
9393
  }
@@ -9870,7 +9684,7 @@ function renderFinalSummary(log, spec, report, fromWizard) {
9870
9684
  const home = process.env.CODEX_HOME ?? join33(homedir3(), ".codex");
9871
9685
  const configPath = join33(home, "config.toml");
9872
9686
  if (!existsSync24(configPath)) return false;
9873
- return hasTrustEntry(readFileSync24(configPath, "utf8"), spec.projectDir);
9687
+ return hasTrustEntry(readFileSync25(configPath, "utf8"), spec.projectDir);
9874
9688
  } catch {
9875
9689
  return false;
9876
9690
  }
@@ -9911,7 +9725,7 @@ function renderFinalSummary(log, spec, report, fromWizard) {
9911
9725
  }
9912
9726
  function hasFillPrompt(path) {
9913
9727
  try {
9914
- return readFileSync24(path, "utf8").includes("<!-- FILL:");
9728
+ return readFileSync25(path, "utf8").includes("<!-- FILL:");
9915
9729
  } catch {
9916
9730
  return false;
9917
9731
  }
@@ -10142,15 +9956,6 @@ function renderPhase1Rows(log, baseline, verbose = false) {
10142
9956
  )
10143
9957
  );
10144
9958
  }
10145
- if (baseline.updateMode.helpersKept?.length) {
10146
- log(
10147
- assetRow(
10148
- "skip",
10149
- "helper is a link",
10150
- `${baseline.updateMode.helpersKept.join(", ")} \xB7 \uB9C1\uD06C\uB77C \uAC31\uC2E0\uD558\uC9C0 \uC54A\uACE0 \uB0A8\uACBC\uB2E4`
10151
- )
10152
- );
10153
- }
10154
9959
  for (const row of outsideLinkRows(baseline.updateMode.outsideLinks ?? [])) log(row);
10155
9960
  if (baseline.updateMode.foreignOwned.length > 0) {
10156
9961
  log(
@@ -10348,6 +10153,9 @@ function renderPhase1Rows(log, baseline, verbose = false) {
10348
10153
  if (j4.backupAbs !== void 0) judgedBackups.add(j4.backupAbs);
10349
10154
  log(judgedRow(j4));
10350
10155
  }
10156
+ for (const f of baseline.shared ?? []) {
10157
+ if (f.backupAbs !== void 0 && f.removedEdited.length > 0) judgedBackups.add(f.backupAbs);
10158
+ }
10351
10159
  for (const row of outsideLinkRows(baseline.outsideLinks ?? [])) log(row);
10352
10160
  for (const row of outOfTrackRows(baseline.outOfTrack)) log(row);
10353
10161
  for (const row of displacedRows(baseline.displacedRestored)) log(row);
@@ -10608,8 +10416,9 @@ function excludedKeyParts(f) {
10608
10416
  parts.push(`removed the harness part: ${f.removedOut.join(", ")} (you asked: ${asked})`);
10609
10417
  }
10610
10418
  if (f.removedEdited.length > 0) {
10419
+ const where = f.backup === void 0 ? "went with it" : `are in the copy: ${f.backup}`;
10611
10420
  parts.push(
10612
- `your edits to ${f.removedEdited.join(", ")} went with it (a hook entry calls a harness script \u2014 left alone it would call a script that is no longer wired)`
10421
+ `your edits to ${f.removedEdited.join(", ")} ${where} (a hook entry calls a harness script \u2014 left alone it would call a script that is no longer wired)`
10613
10422
  );
10614
10423
  }
10615
10424
  if (f.keptOut.length > 0) {
@@ -10729,7 +10538,7 @@ function renderInterruptedInstall(err, e2, cwd = process.cwd()) {
10729
10538
  }
10730
10539
  const { path, error } = e2.record;
10731
10540
  err(
10732
- path !== null ? ` Already in place before it stopped \u2014 recorded in ${relative5(cwd, path) || path}:` : " Already in place before it stopped:"
10541
+ path !== null ? ` Already in place before it stopped \u2014 recorded in ${relative6(cwd, path) || path}:` : " Already in place before it stopped:"
10733
10542
  );
10734
10543
  for (const [key, n] of groups) {
10735
10544
  err(` ${key.endsWith("/") ? `${key} (${n} file${n === 1 ? "" : "s"})` : key}`);
@@ -11052,7 +10861,7 @@ function registerInstallCommand(cli) {
11052
10861
 
11053
10862
  // src/commands/list.ts
11054
10863
  init_esm_shims();
11055
- import { existsSync as existsSync25, readFileSync as readFileSync25 } from "fs";
10864
+ import { existsSync as existsSync25, readFileSync as readFileSync26 } from "fs";
11056
10865
  import { join as join34, resolve as resolve3 } from "path";
11057
10866
  function listAction(options = {}, deps = {}) {
11058
10867
  const log = deps.log ?? console.log;
@@ -11157,7 +10966,7 @@ function formatHarnessFileRows(log, projectDir) {
11157
10966
  const rootMd = log.templates.rootClaudeMd;
11158
10967
  if (rootMd) {
11159
10968
  const path = join34(projectDir, rootMd.path);
11160
- const modified = existsSync25(path) && hashContent(readFileSync25(path, "utf8")) !== rootMd.sha256;
10969
+ const modified = existsSync25(path) && hashContent(readFileSync26(path, "utf8")) !== rootMd.sha256;
11161
10970
  rows2.push(
11162
10971
  modified ? ` ${c.dim(rootMd.path)} ${c.yellow("(\uC218\uC815\uB428 \u2014 uninstall \uC2DC \uBCF4\uC874)")}` : ` ${c.dim(rootMd.path)}`
11163
10972
  );
@@ -11192,9 +11001,9 @@ import {
11192
11001
  accessSync as accessSync2,
11193
11002
  existsSync as existsSync26,
11194
11003
  constants as fsConstants2,
11195
- lstatSync as lstatSync6,
11004
+ lstatSync as lstatSync5,
11196
11005
  readdirSync as readdirSync7,
11197
- readFileSync as readFileSync26,
11006
+ readFileSync as readFileSync27,
11198
11007
  readlinkSync,
11199
11008
  realpathSync as realpathSync3,
11200
11009
  rmdirSync as rmdirSync3,
@@ -11202,7 +11011,7 @@ import {
11202
11011
  statSync as statSync5,
11203
11012
  writeFileSync as writeFileSync9
11204
11013
  } from "fs";
11205
- import { dirname as dirname8, join as join35, relative as relative6, resolve as resolve4, sep as sep6 } from "path";
11014
+ import { dirname as dirname8, join as join35, relative as relative7, resolve as resolve4, sep as sep7 } from "path";
11206
11015
 
11207
11016
  // src/uninstall-interactive.ts
11208
11017
  init_esm_shims();
@@ -12291,7 +12100,8 @@ var TRACK_LABELS = {
12291
12100
  "csr-supabase": "csr-supabase \u2014 Vite + React + Supabase",
12292
12101
  "csr-fastify": "csr-fastify \u2014 Vite + React + Fastify",
12293
12102
  "csr-fastapi": "csr-fastapi \u2014 Vite + React + FastAPI",
12294
- "ssr-htmx": "ssr-htmx \u2014 htmx + FastAPI",
12103
+ "ssr-htmx": "ssr-htmx \u2014 htmx + FastAPI (Python) + daisyUI",
12104
+ "ssr-htmx-fastify": "ssr-htmx-fastify \u2014 htmx + Fastify (Node) + daisyUI",
12295
12105
  "ssr-nextjs": "ssr-nextjs \u2014 Next.js (App Router)",
12296
12106
  data: "data \u2014 Python data / DuckDB / PySide6",
12297
12107
  executive: "executive \u2014 proposals / DD / pitch (no agent-skills)",
@@ -13051,7 +12861,7 @@ function unwritablePaths(ctx) {
13051
12861
  }
13052
12862
  function safeExists(abs) {
13053
12863
  try {
13054
- lstatSync6(abs);
12864
+ lstatSync5(abs);
13055
12865
  return true;
13056
12866
  } catch {
13057
12867
  return false;
@@ -13127,7 +12937,17 @@ function settleLog(ctx, io) {
13127
12937
  return true;
13128
12938
  }
13129
12939
  } else {
13130
- io.rm(join35(projectDir, INSTALL_LOG_DIR));
12940
+ const recordDir = join35(projectDir, INSTALL_LOG_DIR);
12941
+ if (outsideLinkAt(projectDir, INSTALL_LOG_DIR) !== null) io.rm(recordDir);
12942
+ else {
12943
+ for (const rel of [
12944
+ `${INSTALL_LOG_DIR}/${INSTALL_LOG_FILENAME}`,
12945
+ ...HARNESS_RUNTIME_IGNORES
12946
+ ]) {
12947
+ io.rm(join35(projectDir, rel));
12948
+ }
12949
+ pruneEmptyDirsUpward(projectDir, recordDir);
12950
+ }
13131
12951
  if (outsideLinkAt(projectDir, LEGACY_INSTALL_LOG_DIR) === null) {
13132
12952
  io.rm(legacyInstallLogPath(projectDir));
13133
12953
  pruneEmptyDirsUpward(projectDir, join35(projectDir, LEGACY_INSTALL_LOG_DIR));
@@ -13142,7 +12962,9 @@ function dryRunLines(plan, installLog, projectDir, assetsOnly, rootFiles, harnes
13142
12962
  lines2.push(c.dim(" (no project-scope assets to reverse)"));
13143
12963
  }
13144
12964
  lines2.push(...plan.reverseSteps.map((s) => ` \u25CB ${s.label}`));
13145
- lines2.push(" \u25CB remove install record (.uzys-agent-harness/ \u2014 \uB85C\uADF8\xB7\uD5EC\uD37C \uC2A4\uD06C\uB9BD\uD2B8\xB7\uCC28\uB2E8 \uB85C\uADF8)");
12965
+ lines2.push(
12966
+ " \u25CB remove install record (.uzys-agent-harness/ \u2014 \uC124\uCE58 \uAE30\uB85D\xB7\uCC28\uB2E8 \uB85C\uADF8\xB7update \uBC31\uC5C5 \uBAA9\uB85D)"
12967
+ );
13146
12968
  lines2.push(
13147
12969
  ...plan.noReversePath.flatMap((a) => [
13148
12970
  c.dim(` \u2298 ${a.id} (${a.method}) \u2014 \uC790\uB3D9 \uB418\uB3CC\uB9AC\uAE30 \uACBD\uB85C \uC5C6\uC74C, \uAE30\uB85D \uC720\uC9C0`),
@@ -13204,7 +13026,6 @@ function backupArtifactAdvisoryLines(projectDir) {
13204
13026
  }
13205
13027
  }
13206
13028
  for (const dir of BACKUP_SCAN_DIRS) {
13207
- if (dir === INSTALL_LOG_DIR) continue;
13208
13029
  const abs = join35(projectDir, dir);
13209
13030
  if (!existsSync26(abs) || outsideLinkAt(projectDir, dir) !== null) continue;
13210
13031
  for (const rel of listFilesRecursive(abs)) {
@@ -13375,7 +13196,7 @@ function agentsMdRerenderSet(target, remaining, log, projectDir) {
13375
13196
  }
13376
13197
  function readTextOrEmpty(abs) {
13377
13198
  try {
13378
- return readFileSync26(abs, "utf8");
13199
+ return readFileSync27(abs, "utf8");
13379
13200
  } catch {
13380
13201
  return "";
13381
13202
  }
@@ -13516,7 +13337,7 @@ function remnantFor(path, log, harnessRoot, projectDir, wiringGone = []) {
13516
13337
  const gone = (k2) => wiringGone.includes(`hooks/${k2.split("#")[1] ?? ""}`);
13517
13338
  return (disk) => {
13518
13339
  try {
13519
- const template = readFileSync26(join35(harnessRoot, "templates", "settings.json"), "utf8");
13340
+ const template = readFileSync27(join35(harnessRoot, "templates", "settings.json"), "utf8");
13520
13341
  const render = renderSettingsPortion(template, () => true);
13521
13342
  return [...legacySettingsSeed(disk, render, projectDir, { edited: true }).keys()].filter(
13522
13343
  (k2) => !k2.endsWith("{}") && !gone(k2)
@@ -13601,15 +13422,24 @@ function sharedStripLines(results, preview) {
13601
13422
  return id === null ? [] : [id.slice(id.indexOf(":") + 1)];
13602
13423
  };
13603
13424
  const names = (keys) => keys.flatMap(display).join(" \xB7 ");
13425
+ const stays = r.edited === void 0 ? " (yours stays)" : "";
13604
13426
  if (r.fileRemoved) {
13427
+ const only = (verb) => r.edited === void 0 ? ` and only its part ${verb} in it` : "";
13605
13428
  lines2.push(
13606
- preview ? ` \u25CB remove ${r.path} (the harness created it and only its part is in it)` : ` ${status.success(`${r.path} \u2014 removed (the harness created it and only its part was in it)`)}`
13429
+ preview ? ` \u25CB remove ${r.path} (the harness created it${only("is")})` : ` ${status.success(`${r.path} \u2014 removed (the harness created it${only("was")})`)}`
13607
13430
  );
13608
13431
  } else if (r.removed.length > 0) {
13609
13432
  const which = r.path === AGENTS_MD2 ? "" : `: ${names(r.removed)}`;
13610
13433
  const what = r.path === AGENTS_MD2 ? "the harness block" : "the harness part";
13611
13434
  lines2.push(
13612
- preview ? ` \u25CB remove ${what} from ${r.path}${which} (yours stays)` : ` ${status.success(`${r.path} \u2014 removed ${what}${which} (yours stays)`)}`
13435
+ preview ? ` \u25CB remove ${what} from ${r.path}${which}${stays}` : ` ${status.success(`${r.path} \u2014 removed ${what}${which}${stays}`)}`
13436
+ );
13437
+ }
13438
+ if (r.edited !== void 0) {
13439
+ const n = r.edited.keys.length;
13440
+ const edited = `you had edited ${n} harness hook line${n === 1 ? "" : "s"} (${names(r.edited.keys)})`;
13441
+ lines2.push(
13442
+ preview ? ` \u25CB ${r.path} \u2014 ${edited} \u2014 a copy is saved first as ${r.edited.backup}` : ` ${c.yellow("\u2298")} ${r.path} \u2014 ${edited} \u2014 your edit is in the copy: ${r.edited.backup}`
13613
13443
  );
13614
13444
  }
13615
13445
  if (r.kept.length > 0) {
@@ -13802,7 +13632,10 @@ function harnessCandidates(log) {
13802
13632
  const paths = [
13803
13633
  ...(log.policyFiles ?? []).map((f) => `${CLAUDE_DIR3}${f.path}`),
13804
13634
  ...(log.skillFiles ?? []).map((f) => `${SKILLS_DIR3}${f.path}`),
13805
- ...(log.externalFiles ?? []).map((f) => f.path).filter((p2) => !p2.startsWith(`${INSTALL_LOG_DIR}/`)),
13635
+ ...(log.externalFiles ?? []).map((f) => f.path),
13636
+ // 헬퍼 — writer 기록은 `externalFiles` 에 sha 로 적지만 옛 기록(≤v26.161.0)엔 항목이 없다. `recorded()` 가 가른다
13637
+ // (writer 기록에 없으면 `none` = 설치자 파일 · 옛 기록이면 `no-sha` = 번들 판과 같을 때만 백업 없이)
13638
+ ...helperSources().keys(),
13806
13639
  // 어느 필드에도 적힌 적 없는 옛 판의 하네스 파일 — `isTarget` 이 알려 준다(A8 · install 과 같은 방식)
13807
13640
  INSTALLED_TRACKS,
13808
13641
  // 스캔 필드가 아예 없는 옛 로그(v26.125.0 이하)는 필드가 하네스 파일을 열거하지 못한다 — manifest 의 하네스 자리 ·
@@ -13816,11 +13649,18 @@ function harnessTargets(log) {
13816
13649
  const tracked = recordedTrackTargets(log);
13817
13650
  return (p2) => p2 === INSTALLED_TRACKS || tracked(p2);
13818
13651
  }
13652
+ function helperSources() {
13653
+ return new Map(
13654
+ buildManifest({ tracks: [] }).filter((e2) => isCliNeutralTarget(e2.target)).map((e2) => [e2.target, e2.source])
13655
+ );
13656
+ }
13819
13657
  function templateNext(harnessRoot, log, path) {
13820
13658
  if (path === INSTALLED_TRACKS) return installedTracksText(log.spec.tracks.filter(isTrack));
13821
- if (!path.startsWith(CLAUDE_DIR3)) return null;
13659
+ const helper = helperSources().get(path);
13660
+ if (helper === void 0 && !path.startsWith(CLAUDE_DIR3)) return null;
13822
13661
  try {
13823
- return readFileSync26(join35(harnessRoot, "templates", path.slice(CLAUDE_DIR3.length)), "utf8");
13662
+ const source = helper ?? path.slice(CLAUDE_DIR3.length);
13663
+ return readFileSync27(join35(harnessRoot, "templates", source), "utf8");
13824
13664
  } catch {
13825
13665
  return null;
13826
13666
  }
@@ -13846,7 +13686,7 @@ function removeRecordedFile(path, ctx) {
13846
13686
  }
13847
13687
  let st2;
13848
13688
  try {
13849
- st2 = lstatSync6(abs, { throwIfNoEntry: false });
13689
+ st2 = lstatSync5(abs, { throwIfNoEntry: false });
13850
13690
  } catch {
13851
13691
  out.unjudged.push(path);
13852
13692
  return;
@@ -13864,13 +13704,13 @@ function removeRecordedFile(path, ctx) {
13864
13704
  return;
13865
13705
  }
13866
13706
  if (st2.isSymbolicLink()) effective = real;
13867
- disk = readFileSync26(effective, "utf8");
13707
+ disk = readFileSync27(effective, "utf8");
13868
13708
  } catch {
13869
13709
  out.unjudged.push(path);
13870
13710
  return;
13871
13711
  }
13872
13712
  const rec = recorded(log, path, { isTarget: ctx.isTarget });
13873
- const legacy = rec.state !== "sha" && isLegacyHarnessFile(path, hashContent(disk)) && (rec.state === "no-sha" || mayReclaimLegacy(log, path));
13713
+ const legacy = rec.state !== "sha" && isLegacyHarnessFile(path, hashContent(disk)) && (rec.state === "no-sha" || mayReclaimLegacy(log, projectDir, path));
13874
13714
  const inBundle = legacy && ctx.isTarget(bundlePath(path));
13875
13715
  const verdict = judge({
13876
13716
  op: "remove",
@@ -13882,7 +13722,7 @@ function removeRecordedFile(path, ctx) {
13882
13722
  if (verdict.verdict !== "remove" && verdict.verdict !== "backup+remove") return;
13883
13723
  let backup = null;
13884
13724
  if (verdict.verdict === "backup+remove") {
13885
- backup = io.write ? relative6(projectDir, backupFile(effective)).split(sep6).join("/") : `${relative6(projectDir, effective).split(sep6).join("/")}.backup-<time>`;
13725
+ backup = io.write ? relative7(projectDir, backupFile(effective)).split(sep7).join("/") : `${relative7(projectDir, effective).split(sep7).join("/")}.backup-<time>`;
13886
13726
  }
13887
13727
  if (io.write) {
13888
13728
  if (effective !== abs) io.rm(abs);
@@ -13900,14 +13740,13 @@ function legacyFileCandidates() {
13900
13740
  (p2) => p2.startsWith(SKILLS_DIR3) ? [p2, `.agents/skills/${p2.slice(SKILLS_DIR3.length)}`] : [p2]
13901
13741
  );
13902
13742
  }
13903
- function bundlePath(path) {
13904
- return path.startsWith(".agents/skills/") ? `${SKILLS_DIR3}${path.slice(".agents/skills/".length)}` : path;
13905
- }
13906
- function mayReclaimLegacy(log, path) {
13743
+ function mayReclaimLegacy(log, projectDir, path) {
13907
13744
  const clis = installedClis(log);
13908
13745
  if (!ownersOf(path).some((c3) => clis.includes(c3))) return false;
13909
13746
  if (isExcludedPath(bundlePath(path), excludedIds(log))) return false;
13910
- return !(log.rootFiles ?? []).some((f) => f.path === path && f.change === "displaced");
13747
+ return !(log.rootFiles ?? []).some(
13748
+ (f) => f.path === path && f.change === "displaced" && !isLegacyDisplaced(log, projectDir, f)
13749
+ );
13911
13750
  }
13912
13751
  function removeLegacyHookWiring(log, projectDir, scope, io, removed) {
13913
13752
  if (!scope.has(SETTINGS_TARGET) || !installedClis(log).includes("claude")) return [];
@@ -13989,8 +13828,8 @@ function removeHarnessMadeShared(path, sha, projectDir, io, out, pruneFrom) {
13989
13828
  out.unjudged.push(path);
13990
13829
  return true;
13991
13830
  }
13992
- if (lstatSync6(abs).isSymbolicLink()) effective = real;
13993
- current = readFileSync26(effective, "utf8");
13831
+ if (lstatSync5(abs).isSymbolicLink()) effective = real;
13832
+ current = readFileSync27(effective, "utf8");
13994
13833
  } catch {
13995
13834
  out.unjudged.push(path);
13996
13835
  return true;
@@ -14253,7 +14092,7 @@ function removeAnchor(log, projectDir, io) {
14253
14092
  if (outside !== null) return { removed: false, lines: outsideLines([outside], log) };
14254
14093
  let disk;
14255
14094
  try {
14256
- disk = readFileSync26(abs, "utf8");
14095
+ disk = readFileSync27(abs, "utf8");
14257
14096
  } catch {
14258
14097
  return {
14259
14098
  removed: false,
@@ -14286,7 +14125,7 @@ function removeAnchor(log, projectDir, io) {
14286
14125
  ]
14287
14126
  };
14288
14127
  }
14289
- const backup = edited ? relative6(projectDir, backupFile(abs)).split(sep6).join("/") : null;
14128
+ const backup = edited ? relative7(projectDir, backupFile(abs)).split(sep7).join("/") : null;
14290
14129
  io.rm(abs);
14291
14130
  return {
14292
14131
  removed: true,
@@ -14298,7 +14137,7 @@ function removeAnchor(log, projectDir, io) {
14298
14137
  var AGENTS_MD2 = "AGENTS.md";
14299
14138
  function readAgentsMdTemplate(harnessRoot) {
14300
14139
  try {
14301
- return readFileSync26(join35(harnessRoot, AGENTS_MD_TEMPLATE), "utf8");
14140
+ return readFileSync27(join35(harnessRoot, AGENTS_MD_TEMPLATE), "utf8");
14302
14141
  } catch {
14303
14142
  return null;
14304
14143
  }
@@ -14330,8 +14169,8 @@ function removeUnrecordedLegacyAgentsMd(path, projectDir, io, out, pruneFrom) {
14330
14169
  try {
14331
14170
  const real = realpathSync3(abs);
14332
14171
  if (!statSync5(real).isFile()) return false;
14333
- if (lstatSync6(abs).isSymbolicLink()) effective = real;
14334
- current = readFileSync26(effective, "utf8");
14172
+ if (lstatSync5(abs).isSymbolicLink()) effective = real;
14173
+ current = readFileSync27(effective, "utf8");
14335
14174
  } catch {
14336
14175
  return false;
14337
14176
  }
@@ -14349,7 +14188,7 @@ function removeLegacyAgentsMd(path, abs, effective, strip, ctx) {
14349
14188
  let backup = `${path}.backup-<time>`;
14350
14189
  try {
14351
14190
  if (io.write) {
14352
- backup = relative6(projectDir, backupFile(effective)).split(sep6).join("/");
14191
+ backup = relative7(projectDir, backupFile(effective)).split(sep7).join("/");
14353
14192
  if (strip.remainder !== null) {
14354
14193
  writeFileSync9(effective, neutralizeScaffoldBanner(strip.remainder), "utf8");
14355
14194
  } else {
@@ -14379,7 +14218,7 @@ function removeToolFiles(files, projectDir, rm) {
14379
14218
  const abs = join35(projectDir, path);
14380
14219
  let isLink;
14381
14220
  try {
14382
- isLink = lstatSync6(abs).isSymbolicLink();
14221
+ isLink = lstatSync5(abs).isSymbolicLink();
14383
14222
  } catch {
14384
14223
  continue;
14385
14224
  }
@@ -14393,9 +14232,9 @@ function removeToolFiles(files, projectDir, rm) {
14393
14232
  continue;
14394
14233
  }
14395
14234
  try {
14396
- if (hashContent(readFileSync26(abs, "utf8")) !== sha256) {
14235
+ if (hashContent(readFileSync27(abs, "utf8")) !== sha256) {
14397
14236
  const backup = backupFile(abs);
14398
- notes.push(`backed up ${path} \u2192 ${relative6(projectDir, backup)} (changed since install)`);
14237
+ notes.push(`backed up ${path} \u2192 ${relative7(projectDir, backup)} (changed since install)`);
14399
14238
  }
14400
14239
  rm(abs);
14401
14240
  removed.push(path);
@@ -14410,8 +14249,8 @@ function removeToolFiles(files, projectDir, rm) {
14410
14249
  function pruneEmptyDirsUpward(projectDir, startDir) {
14411
14250
  let cursor = startDir;
14412
14251
  while (true) {
14413
- const rel = relative6(projectDir, cursor);
14414
- if (rel === "" || rel.startsWith(`..${sep6}`) || rel === "..") return;
14252
+ const rel = relative7(projectDir, cursor);
14253
+ if (rel === "" || rel.startsWith(`..${sep7}`) || rel === "..") return;
14415
14254
  try {
14416
14255
  if (readdirSync7(cursor).length > 0) return;
14417
14256
  rmdirSync3(cursor);
@@ -14441,7 +14280,7 @@ function readRootClaudeMd(projectDir) {
14441
14280
  const path = join35(projectDir, "CLAUDE.md");
14442
14281
  if (!existsSync26(path)) return null;
14443
14282
  try {
14444
- return readFileSync26(path, "utf8");
14283
+ return readFileSync27(path, "utf8");
14445
14284
  } catch {
14446
14285
  return null;
14447
14286
  }