hunter-harness 0.2.28 → 0.2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +28 -26
  2. package/dist/bin.js +576 -187
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -1216,8 +1216,8 @@ async function assertNoSymlinks(root, relativePath) {
1216
1216
  for (const segment of normalized.split("/")) {
1217
1217
  current = join(current, segment);
1218
1218
  try {
1219
- const stat7 = await lstat(current);
1220
- if (stat7.isSymbolicLink()) {
1219
+ const stat8 = await lstat(current);
1220
+ if (stat8.isSymbolicLink()) {
1221
1221
  throw new UnsafePathError("symbolic links are not managed");
1222
1222
  }
1223
1223
  } catch (error) {
@@ -1233,7 +1233,7 @@ async function assertNoSymlinks(root, relativePath) {
1233
1233
  async function withRetry(action, options) {
1234
1234
  const attempts = options.attempts ?? 3;
1235
1235
  const sleep = options.sleep ?? (async (milliseconds) => {
1236
- await new Promise((resolve10) => setTimeout(resolve10, milliseconds));
1236
+ await new Promise((resolve11) => setTimeout(resolve11, milliseconds));
1237
1237
  });
1238
1238
  let lastError;
1239
1239
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
@@ -2263,6 +2263,12 @@ var MANAGED_NAMES = /* @__PURE__ */ new Set([
2263
2263
  "harness-profile-java.md",
2264
2264
  "harness-profile-java.mdc"
2265
2265
  ]);
2266
+ var AGENT_RULE_ROOTS = [
2267
+ ".claude/rules",
2268
+ ".cursor/rules",
2269
+ ".codebuddy/.rules",
2270
+ ".codebuddy/rules"
2271
+ ];
2266
2272
  function sha256(content) {
2267
2273
  return createHash3("sha256").update(content).digest("hex");
2268
2274
  }
@@ -2284,6 +2290,10 @@ async function readReceipt(root) {
2284
2290
  }
2285
2291
  return { schema_version: 1, source_hashes: {}, targets: {} };
2286
2292
  }
2293
+ async function readManagedProjectRuleProjectionPaths(projectRoot) {
2294
+ const receipt = await readReceipt(resolve3(projectRoot));
2295
+ return new Set(Object.keys(receipt.targets).filter((target) => AGENT_RULE_ROOTS.some((ruleRoot) => target.startsWith(`${ruleRoot}/`))));
2296
+ }
2287
2297
  async function markdownFiles(root) {
2288
2298
  try {
2289
2299
  return (await readdir2(root, { withFileTypes: true })).filter((entry) => entry.isFile() && [".md", ".mdc"].includes(extname(entry.name).toLowerCase())).map((entry) => entry.name).filter((name) => !MANAGED_NAMES.has(name)).sort();
@@ -2293,6 +2303,72 @@ async function markdownFiles(root) {
2293
2303
  throw error;
2294
2304
  }
2295
2305
  }
2306
+ function portable(path) {
2307
+ return path.replaceAll("\\", "/");
2308
+ }
2309
+ function normalizeRuleContent(content) {
2310
+ return content.replace(/\r\n/g, "\n").trimEnd() + "\n";
2311
+ }
2312
+ function canonicalImportContent(content) {
2313
+ const normalized = content.replace(/\r\n/g, "\n");
2314
+ if (!normalized.startsWith("---\n"))
2315
+ return normalizeRuleContent(normalized);
2316
+ const closing = normalized.indexOf("\n---\n", 4);
2317
+ if (closing < 0)
2318
+ return null;
2319
+ const frontmatter = normalized.slice(4, closing);
2320
+ if (/^\s*(?:globs?|paths?)\s*:/im.test(frontmatter) || /^\s*alwaysApply\s*:\s*false\s*$/im.test(frontmatter)) {
2321
+ return null;
2322
+ }
2323
+ return normalizeRuleContent(normalized.slice(closing + 5));
2324
+ }
2325
+ async function collectImportCandidates(root, previous, result) {
2326
+ const candidates = [];
2327
+ for (const ruleRoot of AGENT_RULE_ROOTS) {
2328
+ for (const name of await markdownFiles(join4(root, ...ruleRoot.split("/")))) {
2329
+ const source = portable(`${ruleRoot}/${name}`);
2330
+ const content = await readFile3(join4(root, ...source.split("/")), "utf8");
2331
+ if (previous.targets[source] === sha256(content))
2332
+ continue;
2333
+ const canonical = canonicalImportContent(content);
2334
+ if (canonical === null) {
2335
+ result.agent_specific.push(source);
2336
+ continue;
2337
+ }
2338
+ candidates.push({
2339
+ source,
2340
+ destination: `${RULES_ROOT}/${basename(name, extname(name))}.md`,
2341
+ content: canonical
2342
+ });
2343
+ }
2344
+ }
2345
+ return candidates;
2346
+ }
2347
+ async function importAgentRules(root, canonicalRoot, previous, result) {
2348
+ const grouped = /* @__PURE__ */ new Map();
2349
+ for (const candidate of await collectImportCandidates(root, previous, result)) {
2350
+ const values = grouped.get(candidate.destination) ?? [];
2351
+ values.push(candidate);
2352
+ grouped.set(candidate.destination, values);
2353
+ }
2354
+ for (const [destination, candidates] of [...grouped].sort(([a], [b]) => a.localeCompare(b))) {
2355
+ const destinationPath = join4(canonicalRoot, basename(destination));
2356
+ const current = await optionalText(destinationPath);
2357
+ const distinct = new Set(candidates.map((candidate) => candidate.content));
2358
+ const representative = candidates.at(0);
2359
+ if (representative === void 0)
2360
+ continue;
2361
+ if (current === null && distinct.size === 1) {
2362
+ await atomicWrite(destinationPath, representative.content);
2363
+ result.migrated.push(destination);
2364
+ continue;
2365
+ }
2366
+ if (current !== null && distinct.size === 1 && normalizeRuleContent(current) === representative.content) {
2367
+ continue;
2368
+ }
2369
+ result.conflicts.push(...candidates.map((candidate) => candidate.source));
2370
+ }
2371
+ }
2296
2372
  function targetsFor(name, agents, surface) {
2297
2373
  const stem = basename(name, extname(name));
2298
2374
  const targets = [];
@@ -2319,23 +2395,15 @@ async function synchronizeProjectRules(projectRoot, agents, surface = "both") {
2319
2395
  const canonicalRoot = join4(root, RULES_ROOT);
2320
2396
  const result = {
2321
2397
  migrated: [],
2398
+ agent_specific: [],
2322
2399
  written: [],
2323
2400
  removed: [],
2324
2401
  unchanged: [],
2325
2402
  conflicts: []
2326
2403
  };
2327
2404
  await mkdir4(canonicalRoot, { recursive: true });
2328
- if ((await markdownFiles(canonicalRoot)).length === 0) {
2329
- for (const name of await markdownFiles(join4(root, ".claude", "rules"))) {
2330
- const destination = join4(canonicalRoot, `${basename(name, extname(name))}.md`);
2331
- if (await optionalText(destination) !== null)
2332
- continue;
2333
- const content = await readFile3(join4(root, ".claude", "rules", name), "utf8");
2334
- await atomicWrite(destination, content);
2335
- result.migrated.push(`${RULES_ROOT}/${basename(destination)}`);
2336
- }
2337
- }
2338
2405
  const previous = await readReceipt(root);
2406
+ await importAgentRules(root, canonicalRoot, previous, result);
2339
2407
  const next = { schema_version: 1, source_hashes: {}, targets: {} };
2340
2408
  const desired = /* @__PURE__ */ new Map();
2341
2409
  for (const name of await markdownFiles(canonicalRoot)) {
@@ -2349,9 +2417,10 @@ async function synchronizeProjectRules(projectRoot, agents, surface = "both") {
2349
2417
  const path = join4(root, target);
2350
2418
  const current = await optionalText(path);
2351
2419
  const incomingHash = sha256(content);
2352
- if (current === content) {
2420
+ const canonicalCurrent = current === null ? null : canonicalImportContent(current);
2421
+ if (current === content || canonicalCurrent === content) {
2353
2422
  result.unchanged.push(target);
2354
- next.targets[target] = incomingHash;
2423
+ next.targets[target] = current === null ? incomingHash : sha256(current);
2355
2424
  } else if (current === null || previous.targets[target] === sha256(current)) {
2356
2425
  await atomicWrite(path, content);
2357
2426
  result.written.push(target);
@@ -2411,6 +2480,7 @@ async function synchronizeProjectRules(projectRoot, agents, surface = "both") {
2411
2480
  }
2412
2481
  await atomicWrite(join4(root, RECEIPT_PATH), JSON.stringify(next, null, 2) + "\n");
2413
2482
  result.migrated.sort();
2483
+ result.agent_specific.sort();
2414
2484
  result.written.sort();
2415
2485
  result.removed.sort();
2416
2486
  result.unchanged.sort();
@@ -2918,16 +2988,237 @@ async function enrichContextIndexWithVerification(root, agents, profile, resourc
2918
2988
  await writeFile4(contextPath, JSON.stringify(parsed, null, 2) + "\n", "utf8");
2919
2989
  }
2920
2990
 
2921
- // ../core/dist/project/refresh.js
2991
+ // ../core/dist/project/rule-candidates.js
2922
2992
  import { createHash as createHash6 } from "node:crypto";
2923
- import { readFile as readFile6, readdir as readdir4, rmdir } from "node:fs/promises";
2924
- import { dirname as dirname4, join as join7, resolve as resolve6 } from "node:path";
2993
+ import { mkdir as mkdir5, readFile as readFile6, readdir as readdir4, rename as rename4, stat as stat3, writeFile as writeFile5 } from "node:fs/promises";
2994
+ import { basename as basename3, dirname as dirname4, join as join7, relative, resolve as resolve6 } from "node:path";
2995
+ var ARCHIVE_ROOT = ".harness/archive";
2996
+ var CANDIDATE_PATH = ".harness/knowledge/rule-candidates.json";
2997
+ var MAX_EVIDENCE_BYTES = 2 * 1024 * 1024;
2998
+ var EVIDENCE_NAMES = [
2999
+ /^review-findings.*\.json$/i,
3000
+ /^test-(?:report|results?|failures?).*\.json$/i,
3001
+ /^summary-data\.json$/i
3002
+ ];
3003
+ function sha2562(content) {
3004
+ return createHash6("sha256").update(content).digest("hex");
3005
+ }
3006
+ function portable2(path) {
3007
+ return path.replaceAll("\\", "/");
3008
+ }
3009
+ function normalizeText(value) {
3010
+ return value.replace(/\s+/g, " ").trim();
3011
+ }
3012
+ function safeText(value, limit = 500) {
3013
+ if (typeof value !== "string")
3014
+ return null;
3015
+ const normalized = normalizeText(value).slice(0, limit);
3016
+ if (normalized.length < 8)
3017
+ return null;
3018
+ if (/(?:ignore|disregard)\s+(?:all\s+)?previous|system\s+prompt|developer\s+message/i.test(normalized)) {
3019
+ return null;
3020
+ }
3021
+ if (/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----|gh[pousr]_[A-Za-z0-9]{20,}|(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?):\/\//i.test(normalized)) {
3022
+ return null;
3023
+ }
3024
+ return normalized;
3025
+ }
3026
+ function stringField(record, names) {
3027
+ for (const name of names) {
3028
+ const value = safeText(record[name]);
3029
+ if (value !== null)
3030
+ return value;
3031
+ }
3032
+ return null;
3033
+ }
3034
+ function severityOf(record) {
3035
+ const value = record.severity ?? record.level ?? record.priority ?? "unknown";
3036
+ return typeof value === "string" ? value.toLowerCase() : "unknown";
3037
+ }
3038
+ function highSeverity(severity) {
3039
+ return /^(?:red|critical|high|error|blocker|p0|p1)$/.test(severity);
3040
+ }
3041
+ function evidenceKind(path) {
3042
+ const name = basename3(path).toLowerCase();
3043
+ if (name.startsWith("review-"))
3044
+ return "review";
3045
+ if (name.startsWith("test-"))
3046
+ return "test";
3047
+ return "validation";
3048
+ }
3049
+ function observationFrom(record, path, archive) {
3050
+ const severity = severityOf(record);
3051
+ const suggestion = stringField(record, [
3052
+ "proposed_rule",
3053
+ "proposedRule",
3054
+ "suggestion",
3055
+ "recommendation",
3056
+ "remediation"
3057
+ ]);
3058
+ const issue = stringField(record, ["issue", "message", "error", "failure"]);
3059
+ const title = stringField(record, ["title", "name", "id", "code"]) ?? issue;
3060
+ let proposedRule = suggestion;
3061
+ if (proposedRule === null && issue !== null && highSeverity(severity)) {
3062
+ proposedRule = `\u5FC5\u987B\u589E\u52A0\u53EF\u91CD\u590D\u9A8C\u8BC1\uFF0C\u9632\u6B62\u4EE5\u4E0B\u95EE\u9898\u518D\u6B21\u51FA\u73B0\uFF1A${issue}`;
3063
+ }
3064
+ if (proposedRule === null || title === null)
3065
+ return null;
3066
+ const recordId = record.id ?? record.code ?? record.name ?? null;
3067
+ return {
3068
+ title,
3069
+ proposedRule,
3070
+ severity,
3071
+ evidence: {
3072
+ archive,
3073
+ path,
3074
+ kind: evidenceKind(path),
3075
+ record_id: typeof recordId === "string" ? recordId.slice(0, 120) : null
3076
+ }
3077
+ };
3078
+ }
3079
+ function collectObservations(value, path, archive, output, rejected) {
3080
+ if (Array.isArray(value)) {
3081
+ for (const item2 of value)
3082
+ collectObservations(item2, path, archive, output, rejected);
3083
+ return;
3084
+ }
3085
+ if (value === null || typeof value !== "object")
3086
+ return;
3087
+ const record = value;
3088
+ const candidate = observationFrom(record, path, archive);
3089
+ if (candidate !== null)
3090
+ output.push(candidate);
3091
+ else if (Object.keys(record).some((key) => ["suggestion", "recommendation", "proposed_rule", "proposedRule"].includes(key)))
3092
+ rejected.count += 1;
3093
+ for (const nested of Object.values(record)) {
3094
+ if (nested !== null && typeof nested === "object") {
3095
+ collectObservations(nested, path, archive, output, rejected);
3096
+ }
3097
+ }
3098
+ }
3099
+ async function evidenceFiles(root) {
3100
+ const archiveRoot = join7(root, ...ARCHIVE_ROOT.split("/"));
3101
+ const output = [];
3102
+ async function walk(directory) {
3103
+ let entries;
3104
+ try {
3105
+ entries = await readdir4(directory, { withFileTypes: true });
3106
+ } catch (error) {
3107
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
3108
+ return;
3109
+ throw error;
3110
+ }
3111
+ for (const entry of entries) {
3112
+ const path = join7(directory, entry.name);
3113
+ if (entry.isDirectory())
3114
+ await walk(path);
3115
+ else if (entry.isFile() && EVIDENCE_NAMES.some((pattern) => pattern.test(entry.name))) {
3116
+ if ((await stat3(path)).size <= MAX_EVIDENCE_BYTES)
3117
+ output.push(path);
3118
+ }
3119
+ }
3120
+ }
3121
+ await walk(archiveRoot);
3122
+ return output.sort();
3123
+ }
3124
+ function candidateKey(rule) {
3125
+ return normalizeText(rule).toLocaleLowerCase();
3126
+ }
3127
+ function buildCandidates(observations) {
3128
+ const grouped = /* @__PURE__ */ new Map();
3129
+ for (const observation of observations) {
3130
+ const key = candidateKey(observation.proposedRule);
3131
+ const values = grouped.get(key) ?? [];
3132
+ values.push(observation);
3133
+ grouped.set(key, values);
3134
+ }
3135
+ const candidates = [];
3136
+ for (const [key, values] of grouped) {
3137
+ const archives = new Set(values.map((value) => value.evidence.archive));
3138
+ const highest = values.find((value) => highSeverity(value.severity));
3139
+ if (archives.size < 2 && highest === void 0)
3140
+ continue;
3141
+ const representative = highest ?? values.at(0);
3142
+ if (representative === void 0)
3143
+ continue;
3144
+ const evidence = [...new Map(values.map((value) => [
3145
+ `${value.evidence.path}\0${value.evidence.record_id ?? ""}`,
3146
+ value.evidence
3147
+ ])).values()].sort((a, b) => a.path.localeCompare(b.path));
3148
+ candidates.push({
3149
+ id: `rule_${sha2562(key).slice(0, 16)}`,
3150
+ status: "candidate",
3151
+ title: representative.title,
3152
+ proposed_rule: representative.proposedRule,
3153
+ confidence: archives.size >= 2 && highest !== void 0 ? "high" : "medium",
3154
+ severity: representative.severity,
3155
+ occurrences: evidence.length,
3156
+ evidence
3157
+ });
3158
+ }
3159
+ return candidates.sort((a, b) => a.id.localeCompare(b.id));
3160
+ }
3161
+ async function atomicWrite2(path, content) {
3162
+ await mkdir5(dirname4(path), { recursive: true });
3163
+ const temporary = `${path}.${process.pid}.tmp`;
3164
+ await writeFile5(temporary, content, "utf8");
3165
+ await rename4(temporary, path);
3166
+ }
3167
+ async function synchronizeRuleCandidates(projectRoot, options = {}) {
3168
+ const root = resolve6(projectRoot);
3169
+ const files = await evidenceFiles(root);
3170
+ const sourceHashes = {};
3171
+ const observations = [];
3172
+ const rejected = { count: 0 };
3173
+ for (const path of files) {
3174
+ const relativePath = portable2(relative(root, path));
3175
+ const content2 = await readFile6(path, "utf8");
3176
+ sourceHashes[relativePath] = sha2562(content2);
3177
+ let parsed;
3178
+ try {
3179
+ parsed = JSON.parse(content2);
3180
+ } catch {
3181
+ continue;
3182
+ }
3183
+ const archive = relativePath.split("/")[2] ?? "unknown";
3184
+ collectObservations(parsed, relativePath, archive, observations, rejected);
3185
+ }
3186
+ const manifest = {
3187
+ schema_version: 1,
3188
+ source_hashes: sourceHashes,
3189
+ candidates: buildCandidates(observations)
3190
+ };
3191
+ const content = JSON.stringify(manifest, null, 2) + "\n";
3192
+ const destination = join7(root, ...CANDIDATE_PATH.split("/"));
3193
+ let current = null;
3194
+ try {
3195
+ current = await readFile6(destination, "utf8");
3196
+ } catch (error) {
3197
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
3198
+ throw error;
3199
+ }
3200
+ const changed = current !== content;
3201
+ if (changed && options.dryRun !== true)
3202
+ await atomicWrite2(destination, content);
3203
+ return {
3204
+ path: CANDIDATE_PATH,
3205
+ scanned: files.length,
3206
+ candidates: manifest.candidates.length,
3207
+ changed,
3208
+ rejected_untrusted: rejected.count
3209
+ };
3210
+ }
3211
+
3212
+ // ../core/dist/project/refresh.js
3213
+ import { createHash as createHash7 } from "node:crypto";
3214
+ import { readFile as readFile7, readdir as readdir5, rmdir } from "node:fs/promises";
3215
+ import { dirname as dirname5, join as join8, resolve as resolve7 } from "node:path";
2925
3216
  import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
2926
3217
  var INSTALLED_STATE_PATH = ".harness/state/local/installed-harness-bundle.json";
2927
3218
  var CONTEXT_INDEX_PATH2 = ".harness/context-index.json";
2928
3219
  async function fileHex2(path) {
2929
3220
  try {
2930
- return createHash6("sha256").update(await readFile6(path)).digest("hex");
3221
+ return createHash7("sha256").update(await readFile7(path)).digest("hex");
2931
3222
  } catch (error) {
2932
3223
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
2933
3224
  return null;
@@ -2937,7 +3228,7 @@ async function fileHex2(path) {
2937
3228
  }
2938
3229
  async function readOptionalText(path) {
2939
3230
  try {
2940
- return await readFile6(path, "utf8");
3231
+ return await readFile7(path, "utf8");
2941
3232
  } catch (error) {
2942
3233
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
2943
3234
  return "";
@@ -2946,7 +3237,7 @@ async function readOptionalText(path) {
2946
3237
  }
2947
3238
  }
2948
3239
  async function readInstalledState(root) {
2949
- const content = await readOptionalText(join7(root, INSTALLED_STATE_PATH));
3240
+ const content = await readOptionalText(join8(root, INSTALLED_STATE_PATH));
2950
3241
  if (content === "") {
2951
3242
  return {
2952
3243
  profile: null,
@@ -3016,7 +3307,7 @@ async function readInstalledState(root) {
3016
3307
  };
3017
3308
  }
3018
3309
  async function readInstalledAgentConfiguration(projectRoot) {
3019
- const installed = await readInstalledState(resolve6(projectRoot));
3310
+ const installed = await readInstalledState(resolve7(projectRoot));
3020
3311
  return {
3021
3312
  agents: installed.adapters,
3022
3313
  profiles: Object.fromEntries(installed.adapters.map((agent) => [
@@ -3026,7 +3317,7 @@ async function readInstalledAgentConfiguration(projectRoot) {
3026
3317
  };
3027
3318
  }
3028
3319
  async function readContextIndexBundleHash(root) {
3029
- const content = await readOptionalText(join7(root, CONTEXT_INDEX_PATH2));
3320
+ const content = await readOptionalText(join8(root, CONTEXT_INDEX_PATH2));
3030
3321
  if (content === "")
3031
3322
  return null;
3032
3323
  try {
@@ -3038,13 +3329,13 @@ async function readContextIndexBundleHash(root) {
3038
3329
  }
3039
3330
  }
3040
3331
  async function pruneEmptyParentDirs(root, deletedPaths, boundaryPaths) {
3041
- const boundaries = new Set(boundaryPaths.map((path) => join7(root, path)));
3332
+ const boundaries = new Set(boundaryPaths.map((path) => join8(root, path)));
3042
3333
  for (const deleted of deletedPaths) {
3043
- let dir = dirname4(join7(root, deleted));
3334
+ let dir = dirname5(join8(root, deleted));
3044
3335
  while (dir.startsWith(root) && !boundaries.has(dir)) {
3045
3336
  let entries;
3046
3337
  try {
3047
- entries = await readdir4(dir);
3338
+ entries = await readdir5(dir);
3048
3339
  } catch {
3049
3340
  break;
3050
3341
  }
@@ -3055,7 +3346,7 @@ async function pruneEmptyParentDirs(root, deletedPaths, boundaryPaths) {
3055
3346
  } catch {
3056
3347
  break;
3057
3348
  }
3058
- dir = dirname4(dir);
3349
+ dir = dirname5(dir);
3059
3350
  }
3060
3351
  }
3061
3352
  }
@@ -3082,7 +3373,7 @@ function sortByTarget(items) {
3082
3373
  return [...items].sort((left, right) => left.target_path.localeCompare(right.target_path));
3083
3374
  }
3084
3375
  async function reconcileContextIndex(root, agents, profiles, manifests, codebuddySurface2, verifications) {
3085
- const existing = await readOptionalText(join7(root, CONTEXT_INDEX_PATH2));
3376
+ const existing = await readOptionalText(join8(root, CONTEXT_INDEX_PATH2));
3086
3377
  let codebase = {
3087
3378
  map: ".harness/codebase/map",
3088
3379
  status: "missing"
@@ -3135,7 +3426,7 @@ async function reconcileContextIndex(root, agents, profiles, manifests, codebudd
3135
3426
  }
3136
3427
  async function projectTransitionOperation(root, agents, profiles, codebuddySurface2) {
3137
3428
  const path = ".harness/project.yaml";
3138
- const content = await readOptionalText(join7(root, path));
3429
+ const content = await readOptionalText(join8(root, path));
3139
3430
  if (content === "")
3140
3431
  return null;
3141
3432
  const project = parseYaml3(content);
@@ -3170,11 +3461,11 @@ function mergeTargets(targets) {
3170
3461
  }).sort((left, right) => left.target_path.localeCompare(right.target_path));
3171
3462
  }
3172
3463
  async function reconcileMarkdownBlock(root, fileName, blockId, content, remove, ops, conflicts, preserved) {
3173
- const original = await readOptionalText(join7(root, fileName));
3464
+ const original = await readOptionalText(join8(root, fileName));
3174
3465
  const synthetic = {
3175
3466
  source_path: fileName,
3176
3467
  target_path: fileName,
3177
- sha256: createHash6("sha256").update(content).digest("hex"),
3468
+ sha256: createHash7("sha256").update(content).digest("hex"),
3178
3469
  bytes: new TextEncoder().encode(content)
3179
3470
  };
3180
3471
  let next;
@@ -3189,7 +3480,7 @@ async function reconcileMarkdownBlock(root, fileName, blockId, content, remove,
3189
3480
  next = refreshed.content;
3190
3481
  }
3191
3482
  } catch {
3192
- const current = original === "" ? null : createHash6("sha256").update(original).digest("hex");
3483
+ const current = original === "" ? null : createHash7("sha256").update(original).digest("hex");
3193
3484
  preserved.push(item(synthetic, "preserve", "MALFORMED_MANAGED_BLOCK", current, synthetic.sha256));
3194
3485
  conflicts.push(conflict(synthetic, "MALFORMED_MANAGED_BLOCK", current, synthetic.sha256));
3195
3486
  return;
@@ -3209,7 +3500,7 @@ function stateWithoutInstalledAt(value) {
3209
3500
  return copy;
3210
3501
  }
3211
3502
  async function refreshProject(options) {
3212
- const root = resolve6(options.projectRoot);
3503
+ const root = resolve7(options.projectRoot);
3213
3504
  const installed = await readInstalledState(root);
3214
3505
  const oldAgents = installed.adapters.length > 0 ? installed.adapters : ["claude-code"];
3215
3506
  const selectedAgents = sortHarnessAgents(options.agents);
@@ -3293,7 +3584,7 @@ async function refreshProject(options) {
3293
3584
  const newStateFiles = installed.files.filter((entry) => entry.owner === "shared" || !selectedSet.has(entry.owner));
3294
3585
  for (const target of newManaged) {
3295
3586
  const incoming = target.sha256;
3296
- const current = await fileHex2(join7(root, target.target_path));
3587
+ const current = await fileHex2(join8(root, target.target_path));
3297
3588
  if (current === null) {
3298
3589
  applied.push(item(target, "add", "MISSING_TARGET", null, incoming));
3299
3590
  ops.push({ operation: "add", path: target.target_path, content: target.bytes });
@@ -3321,7 +3612,7 @@ async function refreshProject(options) {
3321
3612
  }
3322
3613
  }
3323
3614
  for (const target of oldOnly) {
3324
- const current = await fileHex2(join7(root, target.target_path));
3615
+ const current = await fileHex2(join8(root, target.target_path));
3325
3616
  if (current === null) {
3326
3617
  continue;
3327
3618
  }
@@ -3364,7 +3655,7 @@ async function refreshProject(options) {
3364
3655
  const verifyEntries = [];
3365
3656
  for (const target of verifyTargets) {
3366
3657
  const rel = target.target_path.replace(/\\/g, "/");
3367
- const actual = await fileHex2(join7(root, target.target_path));
3658
+ const actual = await fileHex2(join8(root, target.target_path));
3368
3659
  verifyEntries.push({ relpath: rel, sha256: actual ?? "" });
3369
3660
  if (actual === null) {
3370
3661
  verifyMismatches.push({ relpath: rel, expected: target.sha256, actual: "<missing>" });
@@ -3397,19 +3688,19 @@ async function refreshProject(options) {
3397
3688
  owner: "shared",
3398
3689
  target_path: "AGENTS.md",
3399
3690
  block_id: AGENTS_CORE_BLOCK_ID,
3400
- content_sha256: createHash6("sha256").update(AGENTS_MANAGED_BLOCK_CONTENT).digest("hex")
3691
+ content_sha256: createHash7("sha256").update(AGENTS_MANAGED_BLOCK_CONTENT).digest("hex")
3401
3692
  },
3402
3693
  ...selectedSet.has("claude-code") ? [{
3403
3694
  owner: "claude-code",
3404
3695
  target_path: "CLAUDE.md",
3405
3696
  block_id: CLAUDE_BLOCK_ID,
3406
- content_sha256: createHash6("sha256").update(CLAUDE_MANAGED_BLOCK_CONTENT).digest("hex")
3697
+ content_sha256: createHash7("sha256").update(CLAUDE_MANAGED_BLOCK_CONTENT).digest("hex")
3407
3698
  }] : [],
3408
3699
  ...selectedSet.has("codebuddy") ? [{
3409
3700
  owner: "codebuddy",
3410
3701
  target_path: "CODEBUDDY.md",
3411
3702
  block_id: CODEBUDDY_BLOCK_ID,
3412
- content_sha256: createHash6("sha256").update(CODEBUDDY_MANAGED_BLOCK_CONTENT).digest("hex")
3703
+ content_sha256: createHash7("sha256").update(CODEBUDDY_MANAGED_BLOCK_CONTENT).digest("hex")
3413
3704
  }] : []
3414
3705
  ].sort((left, right) => left.target_path.localeCompare(right.target_path) || left.block_id.localeCompare(right.block_id));
3415
3706
  const filesByTarget = new Map(newStateFiles.map((entry) => [entry.target_path, entry]));
@@ -3425,7 +3716,7 @@ async function refreshProject(options) {
3425
3716
  files: [...filesByTarget.values()].sort((left, right) => left.target_path.localeCompare(right.target_path) || left.source_path.localeCompare(right.source_path)),
3426
3717
  managed_blocks: managedBlocks
3427
3718
  };
3428
- const existingState = await readOptionalText(join7(root, INSTALLED_STATE_PATH));
3719
+ const existingState = await readOptionalText(join8(root, INSTALLED_STATE_PATH));
3429
3720
  let existingParsed = null;
3430
3721
  try {
3431
3722
  existingParsed = existingState === "" ? null : JSON.parse(existingState);
@@ -3480,7 +3771,7 @@ function buildMarkerCoreHash(text) {
3480
3771
  }
3481
3772
  }
3482
3773
  async function collectFreshness(options) {
3483
- const root = resolve6(options.projectRoot);
3774
+ const root = resolve7(options.projectRoot);
3484
3775
  const installed = await readInstalledState(root);
3485
3776
  const codebuddySurface2 = options.codebuddySurface ?? "both";
3486
3777
  const agents = [];
@@ -3525,18 +3816,18 @@ async function collectFreshness(options) {
3525
3816
  identity.adapterHash = sha256Bytes(canonicalJson(targets.map((target) => ({ path: target.target_path.replace(/\\/g, "/"), sha256: target.sha256 })).sort((a, b) => a.path.localeCompare(b.path))));
3526
3817
  const installedProjection = await Promise.all(targets.map(async (target) => ({
3527
3818
  path: target.target_path.replace(/\\/g, "/"),
3528
- sha256: await fileHex2(join7(root, target.target_path))
3819
+ sha256: await fileHex2(join8(root, target.target_path))
3529
3820
  })));
3530
3821
  identity.installedAdapterHash = sha256Bytes(canonicalJson(installedProjection.sort((a, b) => a.path.localeCompare(b.path))));
3531
3822
  const markerTarget = targets.find((target) => target.target_path.replace(/\\/g, "/").endsWith(`/${BUILD_MARKER_BUNDLE_PATH}`) || target.target_path === BUILD_MARKER_BUNDLE_PATH);
3532
3823
  if (markerTarget !== void 0) {
3533
- identity.installedCoreHash = buildMarkerCoreHash(await readOptionalText(join7(root, markerTarget.target_path)));
3824
+ identity.installedCoreHash = buildMarkerCoreHash(await readOptionalText(join8(root, markerTarget.target_path)));
3534
3825
  }
3535
3826
  const mismatchDetails = [];
3536
3827
  const contentEntries = [];
3537
3828
  for (const target of targets) {
3538
3829
  const rel = target.target_path.replace(/\\/g, "/");
3539
- const actual = await fileHex2(join7(root, target.target_path));
3830
+ const actual = await fileHex2(join8(root, target.target_path));
3540
3831
  contentEntries.push({ relpath: rel, sha256: actual ?? "" });
3541
3832
  if (actual === null) {
3542
3833
  mismatchDetails.push({ relpath: rel, expected: target.sha256, actual: "<missing>" });
@@ -3559,7 +3850,7 @@ async function collectFreshness(options) {
3559
3850
  const drifted = [];
3560
3851
  const missing = [];
3561
3852
  for (const target of targets) {
3562
- const current = await fileHex2(join7(root, target.target_path));
3853
+ const current = await fileHex2(join8(root, target.target_path));
3563
3854
  if (current === null) {
3564
3855
  missing.push(target.target_path);
3565
3856
  } else if (current !== target.sha256) {
@@ -3807,11 +4098,11 @@ function rawFindings(content) {
3807
4098
  if (rule.id === "HH_PASSWORD_VALUE" && isObviousPlaceholder(value)) {
3808
4099
  continue;
3809
4100
  }
3810
- const relative3 = match[0].indexOf(value);
4101
+ const relative4 = match[0].indexOf(value);
3811
4102
  findings.push({
3812
4103
  ruleId: rule.id,
3813
4104
  severity: rule.severity,
3814
- offset: match.index + Math.max(0, relative3),
4105
+ offset: match.index + Math.max(0, relative4),
3815
4106
  value
3816
4107
  });
3817
4108
  }
@@ -3923,8 +4214,8 @@ function generateProposalPreview(input, scanOptions = {}) {
3923
4214
  }
3924
4215
 
3925
4216
  // ../core/dist/push/credentials.js
3926
- import { readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
3927
- import { join as join8 } from "node:path";
4217
+ import { readFile as readFile8, writeFile as writeFile6 } from "node:fs/promises";
4218
+ import { join as join9 } from "node:path";
3928
4219
  import { parse as parseYaml4, stringify as stringifyYaml3 } from "yaml";
3929
4220
  var CREDENTIALS_LOCAL_RELATIVE = ".harness/credentials.local.yaml";
3930
4221
  var CREDENTIALS_GITIGNORE_LINES = [
@@ -3994,7 +4285,7 @@ function mergeLocalCredentials(existing, patch) {
3994
4285
  }
3995
4286
  async function readLocalCredentials(projectRoot) {
3996
4287
  try {
3997
- const raw = await readFile7(join8(projectRoot, CREDENTIALS_LOCAL_RELATIVE), "utf8");
4288
+ const raw = await readFile8(join9(projectRoot, CREDENTIALS_LOCAL_RELATIVE), "utf8");
3998
4289
  return parseLocalCredentials(parseYaml4(raw));
3999
4290
  } catch (error) {
4000
4291
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -4005,13 +4296,13 @@ async function readLocalCredentials(projectRoot) {
4005
4296
  }
4006
4297
  async function writeLocalCredentials(projectRoot, credentials) {
4007
4298
  const normalized = validateLocalCredentials(credentials);
4008
- await writeFile5(join8(projectRoot, CREDENTIALS_LOCAL_RELATIVE), stringifyYaml3(normalized, { sortMapEntries: true }) + "\n", "utf8");
4299
+ await writeFile6(join9(projectRoot, CREDENTIALS_LOCAL_RELATIVE), stringifyYaml3(normalized, { sortMapEntries: true }) + "\n", "utf8");
4009
4300
  }
4010
4301
  async function ensureCredentialsGitignore(projectRoot) {
4011
- const gitignorePath = join8(projectRoot, ".gitignore");
4302
+ const gitignorePath = join9(projectRoot, ".gitignore");
4012
4303
  let content = "";
4013
4304
  try {
4014
- content = await readFile7(gitignorePath, "utf8");
4305
+ content = await readFile8(gitignorePath, "utf8");
4015
4306
  } catch (error) {
4016
4307
  if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
4017
4308
  throw error;
@@ -4034,7 +4325,7 @@ async function ensureCredentialsGitignore(projectRoot) {
4034
4325
  return;
4035
4326
  }
4036
4327
  const suffix = content.length === 0 || content.endsWith("\n") ? "" : "\n";
4037
- await writeFile5(gitignorePath, content + suffix + missing.join("\n") + "\n", "utf8");
4328
+ await writeFile6(gitignorePath, content + suffix + missing.join("\n") + "\n", "utf8");
4038
4329
  }
4039
4330
  function resolvePushAuth(input) {
4040
4331
  const tokenEnv = input.tokenEnv ?? input.projectTokenEnv;
@@ -4062,28 +4353,28 @@ function resolvePushAuth(input) {
4062
4353
  }
4063
4354
 
4064
4355
  // ../core/dist/push/push.js
4065
- import { lstat as lstat3, readFile as readFile11, readdir as readdir5, rm as rm5 } from "node:fs/promises";
4066
- import { join as join12, relative, resolve as resolve8 } from "node:path";
4356
+ import { lstat as lstat3, readFile as readFile12, readdir as readdir6, rm as rm5 } from "node:fs/promises";
4357
+ import { join as join13, relative as relative2, resolve as resolve9 } from "node:path";
4067
4358
  import { parse as parseYaml5, stringify as stringifyYaml4 } from "yaml";
4068
4359
 
4069
4360
  // ../core/dist/state/baseline.js
4070
- import { readFile as readFile8 } from "node:fs/promises";
4071
- import { join as join9 } from "node:path";
4361
+ import { readFile as readFile9 } from "node:fs/promises";
4362
+ import { join as join10 } from "node:path";
4072
4363
  async function readBaseline(projectRoot) {
4073
- const content = await readFile8(join9(stateLayout(projectRoot).baseline, "manifest.json"), "utf8");
4364
+ const content = await readFile9(join10(stateLayout(projectRoot).baseline, "manifest.json"), "utf8");
4074
4365
  return baselineManifestSchema.parse(JSON.parse(content));
4075
4366
  }
4076
4367
 
4077
4368
  // ../core/dist/state/locks.js
4078
4369
  import { randomUUID as randomUUID3 } from "node:crypto";
4079
- import { readFile as readFile9, rename as rename4, rm as rm3, writeFile as writeFile6 } from "node:fs/promises";
4080
- import { join as join10 } from "node:path";
4370
+ import { readFile as readFile10, rename as rename5, rm as rm3, writeFile as writeFile7 } from "node:fs/promises";
4371
+ import { join as join11 } from "node:path";
4081
4372
  async function readLock(path) {
4082
- return JSON.parse(await readFile9(path, "utf8"));
4373
+ return JSON.parse(await readFile10(path, "utf8"));
4083
4374
  }
4084
4375
  async function acquireProtocolLock(projectRoot, operation, options = {}) {
4085
4376
  const layout = await ensureStateLayout(projectRoot);
4086
- const lockPath = join10(layout.locks, "protocol.lock");
4377
+ const lockPath = join11(layout.locks, "protocol.lock");
4087
4378
  const now = options.now ?? Date.now();
4088
4379
  const staleAfterMs = options.staleAfterMs ?? 15 * 60 * 1e3;
4089
4380
  const record = {
@@ -4096,7 +4387,7 @@ async function acquireProtocolLock(projectRoot, operation, options = {}) {
4096
4387
  };
4097
4388
  for (let attempt = 0; attempt < 2; attempt += 1) {
4098
4389
  try {
4099
- await writeFile6(lockPath, JSON.stringify(record, null, 2), { flag: "wx" });
4390
+ await writeFile7(lockPath, JSON.stringify(record, null, 2), { flag: "wx" });
4100
4391
  return {
4101
4392
  path: lockPath,
4102
4393
  operation,
@@ -4118,15 +4409,15 @@ async function acquireProtocolLock(projectRoot, operation, options = {}) {
4118
4409
  if (now - current.heartbeat_at_ms <= staleAfterMs) {
4119
4410
  throw new Error("protocol lock is active for operation " + current.operation, { cause: error });
4120
4411
  }
4121
- await rename4(lockPath, lockPath + ".stale-" + randomUUID3());
4412
+ await rename5(lockPath, lockPath + ".stale-" + randomUUID3());
4122
4413
  }
4123
4414
  }
4124
4415
  throw new Error("unable to acquire protocol lock");
4125
4416
  }
4126
4417
 
4127
4418
  // ../core/dist/sync/synchronize.js
4128
- import { lstat as lstat2, readFile as readFile10, rm as rm4 } from "node:fs/promises";
4129
- import { join as join11, resolve as resolve7 } from "node:path";
4419
+ import { lstat as lstat2, readFile as readFile11, rm as rm4 } from "node:fs/promises";
4420
+ import { join as join12, resolve as resolve8 } from "node:path";
4130
4421
 
4131
4422
  // ../core/dist/update/conflicts.js
4132
4423
  function operationTargetPath(operation) {
@@ -4304,7 +4595,8 @@ function planArtifactRebase(input) {
4304
4595
  plan.alreadyApplied.push({ path: target, operation });
4305
4596
  continue;
4306
4597
  }
4307
- const staticDecision = decideUpdate(policy, false);
4598
+ const projectionManaged = input.protocolOnlyPaths?.has(source) === true || input.protocolOnlyPaths?.has(target) === true;
4599
+ const staticDecision = projectionManaged ? { apply: false, reason: "protocol-only" } : decideUpdate(policy, false);
4308
4600
  if (!staticDecision.apply) {
4309
4601
  const acknowledgeReason = staticDecision.reason === "protocol-only" ? "protocol-only" : "policy-never";
4310
4602
  const entry2 = acknowledgedEntry(operation, context, input.projectVersion, acknowledgeReason);
@@ -4408,8 +4700,8 @@ async function pathExists(path) {
4408
4700
  }
4409
4701
  }
4410
4702
  async function optionalContent(root, path) {
4411
- const full = join11(root, path);
4412
- return await pathExists(full) ? readFile10(full, "utf8") : null;
4703
+ const full = join12(root, path);
4704
+ return await pathExists(full) ? readFile11(full, "utf8") : null;
4413
4705
  }
4414
4706
  function manifestPayloadHash(manifest) {
4415
4707
  const payload = { ...manifest };
@@ -4421,10 +4713,10 @@ async function loadBlob(root, client, artifactId, operation, requestId, dryRun)
4421
4713
  return null;
4422
4714
  }
4423
4715
  const hash = operation.content_sha256;
4424
- const cacheRoot = join11(root, ".harness", "cache", "server-artifacts", artifactId);
4425
- const cachePath = join11(cacheRoot, "blobs", hash.replace(":", "_"));
4716
+ const cacheRoot = join12(root, ".harness", "cache", "server-artifacts", artifactId);
4717
+ const cachePath = join12(cacheRoot, "blobs", hash.replace(":", "_"));
4426
4718
  if (await pathExists(cachePath) && await sha256File(cachePath) === hash) {
4427
- return readFile10(cachePath, "utf8");
4719
+ return readFile11(cachePath, "utf8");
4428
4720
  }
4429
4721
  const bytes = await client.downloadArtifactBlob(artifactId, hash, requestId);
4430
4722
  if (bytes.byteLength !== operation.size_bytes || sha256Bytes(bytes) !== hash) {
@@ -4515,7 +4807,7 @@ function applyBaselineUpdates(baseline, plan) {
4515
4807
  }
4516
4808
  return next;
4517
4809
  }
4518
- async function planSingleArtifact(root, baseline, manifest, client, requestId, dryRun, conflictStrategy, resolveOverrides) {
4810
+ async function planSingleArtifact(root, baseline, manifest, client, requestId, dryRun, conflictStrategy, resolveOverrides, protocolOnlyPaths) {
4519
4811
  if (manifest.project_version === null) {
4520
4812
  throw new Error("artifact manifest missing project_version");
4521
4813
  }
@@ -4525,11 +4817,12 @@ async function planSingleArtifact(root, baseline, manifest, client, requestId, d
4525
4817
  projectVersion: manifest.project_version,
4526
4818
  contexts,
4527
4819
  conflictStrategy,
4528
- ...resolveOverrides === void 0 ? {} : { resolveOverrides }
4820
+ ...resolveOverrides === void 0 ? {} : { resolveOverrides },
4821
+ ...protocolOnlyPaths === void 0 ? {} : { protocolOnlyPaths }
4529
4822
  });
4530
4823
  }
4531
4824
  async function saveConflictReport(root, requestId, manifest, plan) {
4532
- const reportPath = join11(root, ".harness", "reports", "conflicts-" + requestId + ".json");
4825
+ const reportPath = join12(root, ".harness", "reports", "conflicts-" + requestId + ".json");
4533
4826
  await atomicWriteJson(reportPath, {
4534
4827
  schema_version: 1,
4535
4828
  request_id: requestId,
@@ -4540,7 +4833,7 @@ async function saveConflictReport(root, requestId, manifest, plan) {
4540
4833
  });
4541
4834
  }
4542
4835
  async function synchronizeArtifacts(options, initialBaseline) {
4543
- const root = resolve7(options.projectRoot);
4836
+ const root = resolve8(options.projectRoot);
4544
4837
  const projectId = options.project.project.project_id;
4545
4838
  if (projectId === null) {
4546
4839
  throw new Error("PROJECT_NOT_BOUND");
@@ -4589,7 +4882,7 @@ async function synchronizeArtifacts(options, initialBaseline) {
4589
4882
  if (manifest.artifact_id !== discovery.artifact_id || manifest.project_id !== projectId || manifest.project_version === null || manifestPayloadHash(manifest) !== manifest.manifest_sha256) {
4590
4883
  throw new Error("ARTIFACT_HASH_MISMATCH");
4591
4884
  }
4592
- let plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, conflictStrategy, options.resolveOverrides);
4885
+ let plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, conflictStrategy, options.resolveOverrides, options.protocolOnlyPaths);
4593
4886
  if (plan.conflicts.length > 0 && options.confirmConflictStrategy !== void 0) {
4594
4887
  const confirmed = await options.confirmConflictStrategy(plan.conflicts);
4595
4888
  if (confirmed === false) {
@@ -4603,7 +4896,7 @@ async function synchronizeArtifacts(options, initialBaseline) {
4603
4896
  aggregate.artifactId = manifest.artifact_id;
4604
4897
  return aggregate;
4605
4898
  }
4606
- plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, confirmed, options.resolveOverrides);
4899
+ plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, confirmed, options.resolveOverrides, options.protocolOnlyPaths);
4607
4900
  }
4608
4901
  const paths = pathsFromPlan(plan);
4609
4902
  aggregate.applied.push(...paths.applied);
@@ -4647,7 +4940,7 @@ async function synchronizeArtifacts(options, initialBaseline) {
4647
4940
  }
4648
4941
  continue;
4649
4942
  }
4650
- await atomicWriteJson(join11(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
4943
+ await atomicWriteJson(join12(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
4651
4944
  const lock = await acquireProtocolLock(root, "update", { requestId: options.requestId });
4652
4945
  try {
4653
4946
  const nextBaseline = applyBaselineUpdates(baseline, plan);
@@ -4717,7 +5010,7 @@ function baselineEntryFromOperation(manifest, operation, localContent) {
4717
5010
  };
4718
5011
  }
4719
5012
  async function advanceBaselineFromArtifact(options, baseline) {
4720
- const root = resolve7(options.projectRoot);
5013
+ const root = resolve8(options.projectRoot);
4721
5014
  if (options.manifest.project_version === null) {
4722
5015
  throw new Error("artifact manifest missing project_version");
4723
5016
  }
@@ -4818,29 +5111,29 @@ async function walkFiles(root, current, output) {
4818
5111
  if (!await exists3(current)) {
4819
5112
  return;
4820
5113
  }
4821
- for (const item2 of await readdir5(current, { withFileTypes: true })) {
4822
- const path = join12(current, item2.name);
5114
+ for (const item2 of await readdir6(current, { withFileTypes: true })) {
5115
+ const path = join13(current, item2.name);
4823
5116
  if (item2.isSymbolicLink()) {
4824
5117
  throw new PushWorkflowError("symlink is not pushable", 6, "UNSAFE_SYMLINK");
4825
5118
  }
4826
5119
  if (item2.isDirectory()) {
4827
5120
  await walkFiles(root, path, output);
4828
5121
  } else if (item2.isFile()) {
4829
- output.push(normalizeManagedPath(relative(root, path).replaceAll("\\", "/")));
5122
+ output.push(normalizeManagedPath(relative2(root, path).replaceAll("\\", "/")));
4830
5123
  }
4831
5124
  }
4832
5125
  }
4833
5126
  async function walkArchiveSummaries(root, output) {
4834
- const archiveRoot = join12(root, ".harness", "archive");
5127
+ const archiveRoot = join13(root, ".harness", "archive");
4835
5128
  if (!await exists3(archiveRoot))
4836
5129
  return;
4837
- for (const item2 of await readdir5(archiveRoot, { withFileTypes: true })) {
5130
+ for (const item2 of await readdir6(archiveRoot, { withFileTypes: true })) {
4838
5131
  if (item2.isSymbolicLink()) {
4839
5132
  throw new PushWorkflowError("symlink is not pushable", 6, "UNSAFE_SYMLINK");
4840
5133
  }
4841
5134
  if (!item2.isDirectory())
4842
5135
  continue;
4843
- const summaryPath = join12(archiveRoot, item2.name, "reports", "final", "summary-data.json");
5136
+ const summaryPath = join13(archiveRoot, item2.name, "reports", "final", "summary-data.json");
4844
5137
  try {
4845
5138
  const stats = await lstat3(summaryPath);
4846
5139
  if (stats.isSymbolicLink()) {
@@ -4853,7 +5146,7 @@ async function walkArchiveSummaries(root, output) {
4853
5146
  continue;
4854
5147
  throw error;
4855
5148
  }
4856
- const relativePath = normalizeManagedPath(relative(root, summaryPath).replaceAll("\\", "/"));
5149
+ const relativePath = normalizeManagedPath(relative2(root, summaryPath).replaceAll("\\", "/"));
4857
5150
  if (ARCHIVE_SUMMARY_PATH.test(relativePath)) {
4858
5151
  output.push(relativePath);
4859
5152
  }
@@ -4868,19 +5161,19 @@ function enabledHarnessAgents(project) {
4868
5161
  async function walkHarnessEntries(root, directory, output) {
4869
5162
  if (!await exists3(directory))
4870
5163
  return;
4871
- for (const item2 of await readdir5(directory, { withFileTypes: true })) {
5164
+ for (const item2 of await readdir6(directory, { withFileTypes: true })) {
4872
5165
  if (item2.name.startsWith("harness-")) {
4873
- const path = join12(directory, item2.name);
5166
+ const path = join13(directory, item2.name);
4874
5167
  if (item2.isDirectory()) {
4875
5168
  await walkFiles(root, path, output);
4876
5169
  } else if (item2.isFile()) {
4877
- output.push(normalizeManagedPath(relative(root, path).replaceAll("\\", "/")));
5170
+ output.push(normalizeManagedPath(relative2(root, path).replaceAll("\\", "/")));
4878
5171
  }
4879
5172
  }
4880
5173
  }
4881
5174
  }
4882
5175
  async function managedFiles(projectRoot, project) {
4883
- const root = resolve8(projectRoot);
5176
+ const root = resolve9(projectRoot);
4884
5177
  const paths = [];
4885
5178
  const adapters = getAdapters(enabledHarnessAgents(project));
4886
5179
  const managedFiles2 = [
@@ -4889,26 +5182,26 @@ async function managedFiles(projectRoot, project) {
4889
5182
  ...adapters.some((adapter) => adapter.name === "codebuddy") ? ["CODEBUDDY.md"] : []
4890
5183
  ];
4891
5184
  for (const path of managedFiles2) {
4892
- if (await exists3(join12(root, path))) {
5185
+ if (await exists3(join13(root, path))) {
4893
5186
  paths.push(path);
4894
5187
  }
4895
5188
  }
4896
5189
  for (const path of SHARED_MANAGED_ROOTS) {
4897
- await walkFiles(root, join12(root, path), paths);
5190
+ await walkFiles(root, join13(root, path), paths);
4898
5191
  }
4899
5192
  await walkArchiveSummaries(root, paths);
4900
5193
  for (const adapter of adapters) {
4901
5194
  if (adapter.rulesRoot !== null) {
4902
- await walkHarnessEntries(root, join12(root, adapter.rulesRoot), paths);
5195
+ await walkHarnessEntries(root, join13(root, adapter.rulesRoot), paths);
4903
5196
  }
4904
- await walkHarnessEntries(root, join12(root, adapter.skillsRoot), paths);
5197
+ await walkHarnessEntries(root, join13(root, adapter.skillsRoot), paths);
4905
5198
  if (adapter.agentsRoot !== null) {
4906
- await walkHarnessEntries(root, join12(root, adapter.agentsRoot), paths);
5199
+ await walkHarnessEntries(root, join13(root, adapter.agentsRoot), paths);
4907
5200
  }
4908
5201
  }
4909
5202
  const result = {};
4910
5203
  for (const path of [...new Set(paths)].sort()) {
4911
- result[path] = await readFile11(join12(root, path), "utf8");
5204
+ result[path] = await readFile12(join13(root, path), "utf8");
4912
5205
  }
4913
5206
  return result;
4914
5207
  }
@@ -4917,14 +5210,14 @@ function proposalBaseline(baseline) {
4917
5210
  }
4918
5211
  async function readProject(root) {
4919
5212
  try {
4920
- return projectConfigSchema.parse(parseYaml5(await readFile11(join12(root, ".harness", "project.yaml"), "utf8")));
5213
+ return projectConfigSchema.parse(parseYaml5(await readFile12(join13(root, ".harness", "project.yaml"), "utf8")));
4921
5214
  } catch {
4922
5215
  throw new PushWorkflowError("project configuration is missing or invalid", 3, "PROJECT_CONFIG_INVALID");
4923
5216
  }
4924
5217
  }
4925
5218
  async function readOptionalJson(path) {
4926
5219
  try {
4927
- return JSON.parse(await readFile11(path, "utf8"));
5220
+ return JSON.parse(await readFile12(path, "utf8"));
4928
5221
  } catch (error) {
4929
5222
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
4930
5223
  return null;
@@ -4936,7 +5229,7 @@ async function clientIdFor(root, explicit) {
4936
5229
  if (explicit !== void 0) {
4937
5230
  return explicit;
4938
5231
  }
4939
- const path = join12(root, ".harness", "state", "local", "client.json");
5232
+ const path = join13(root, ".harness", "state", "local", "client.json");
4940
5233
  const existing = await readOptionalJson(path);
4941
5234
  if (typeof existing?.client_id === "string" && /^cli_[A-Za-z0-9_-]+$/.test(existing.client_id)) {
4942
5235
  return existing.client_id;
@@ -5132,7 +5425,7 @@ async function bindProject(root, project, baseline, projectId) {
5132
5425
  return { project: nextProject, baseline: nextBaseline };
5133
5426
  }
5134
5427
  async function pushProject(options) {
5135
- const root = resolve8(options.projectRoot);
5428
+ const root = resolve9(options.projectRoot);
5136
5429
  let project = await readProject(root);
5137
5430
  let baseline = await readBaseline(root);
5138
5431
  const profile = parseHarnessProfile(project.project.profiles[0]);
@@ -5198,7 +5491,7 @@ async function pushProject(options) {
5198
5491
  if (options.confirmProposal !== void 0 && !await options.confirmProposal(preview)) {
5199
5492
  return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
5200
5493
  }
5201
- const workflowPath = join12(root, ".harness", "state", "local", "push-workflow.json");
5494
+ const workflowPath = join13(root, ".harness", "state", "local", "push-workflow.json");
5202
5495
  const priorWorkflow = await readOptionalJson(workflowPath);
5203
5496
  const provisionalRequestId = priorWorkflow?.local_project_key === project.project.local_project_key ? priorWorkflow.request_id : uuidV7();
5204
5497
  const lock = await acquireProtocolLock(root, "push", { requestId: provisionalRequestId });
@@ -5353,7 +5646,7 @@ async function pushProject(options) {
5353
5646
  pushWarning = "BASELINE_ADVANCE_DEFERRED";
5354
5647
  }
5355
5648
  }
5356
- await atomicWriteJson(join12(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
5649
+ await atomicWriteJson(join13(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
5357
5650
  schema_version: 1,
5358
5651
  request_id: requestId,
5359
5652
  project_id: projectId,
@@ -5398,14 +5691,14 @@ async function pushProject(options) {
5398
5691
  }
5399
5692
 
5400
5693
  // ../core/dist/state/cleanup.js
5401
- import { readFile as readFile12, readdir as readdir6, rm as rm6 } from "node:fs/promises";
5402
- import { join as join13 } from "node:path";
5694
+ import { readFile as readFile13, readdir as readdir7, rm as rm6 } from "node:fs/promises";
5695
+ import { join as join14 } from "node:path";
5403
5696
  function isSafeEntryName(name) {
5404
5697
  return name.length > 0 && !name.includes("/") && !name.includes("\\") && name !== "." && name !== ".." && !name.includes("\0");
5405
5698
  }
5406
5699
  async function listDir(path) {
5407
5700
  try {
5408
- return await readdir6(path);
5701
+ return await readdir7(path);
5409
5702
  } catch (error) {
5410
5703
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
5411
5704
  return [];
@@ -5423,7 +5716,7 @@ async function cleanupProject(options) {
5423
5716
  continue;
5424
5717
  let journal;
5425
5718
  try {
5426
- journal = JSON.parse(await readFile12(join13(layout.transactions, name, "journal.json"), "utf8"));
5719
+ journal = JSON.parse(await readFile13(join14(layout.transactions, name, "journal.json"), "utf8"));
5427
5720
  } catch {
5428
5721
  continue;
5429
5722
  }
@@ -5441,7 +5734,7 @@ async function cleanupProject(options) {
5441
5734
  continue;
5442
5735
  pruned.push(entry.id);
5443
5736
  if (!options.dryRun) {
5444
- await rm6(join13(layout.transactions, entry.id), { recursive: true, force: true });
5737
+ await rm6(join14(layout.transactions, entry.id), { recursive: true, force: true });
5445
5738
  }
5446
5739
  }
5447
5740
  }
@@ -5450,7 +5743,7 @@ async function cleanupProject(options) {
5450
5743
  continue;
5451
5744
  removedCache.push(name);
5452
5745
  if (!options.dryRun) {
5453
- await rm6(join13(layout.serverArtifacts, name), { recursive: true, force: true });
5746
+ await rm6(join14(layout.serverArtifacts, name), { recursive: true, force: true });
5454
5747
  }
5455
5748
  }
5456
5749
  return {
@@ -5510,10 +5803,10 @@ var SKILL_TARGET_AGENTS = Object.freeze([
5510
5803
  ]);
5511
5804
 
5512
5805
  // ../core/dist/transaction/recovery.js
5513
- import { readFile as readFile13, readdir as readdir7, rm as rm7, stat as stat3 } from "node:fs/promises";
5514
- import { join as join14 } from "node:path";
5806
+ import { readFile as readFile14, readdir as readdir8, rm as rm7, stat as stat4 } from "node:fs/promises";
5807
+ import { join as join15 } from "node:path";
5515
5808
  async function recoverTransaction(projectRoot, transactionId) {
5516
- const journal = JSON.parse(await readFile13(join14(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
5809
+ const journal = JSON.parse(await readFile14(join15(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
5517
5810
  if (journal.state === "committed") {
5518
5811
  return { transactionId, status: "committed" };
5519
5812
  }
@@ -5530,7 +5823,7 @@ async function listTransactions(projectRoot) {
5530
5823
  const root = stateLayout(projectRoot).transactions;
5531
5824
  let names;
5532
5825
  try {
5533
- names = await readdir7(root);
5826
+ names = await readdir8(root);
5534
5827
  } catch (error) {
5535
5828
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
5536
5829
  return [];
@@ -5540,7 +5833,7 @@ async function listTransactions(projectRoot) {
5540
5833
  const transactions = [];
5541
5834
  for (const transactionId of names) {
5542
5835
  try {
5543
- const journal = JSON.parse(await readFile13(join14(root, transactionId, "journal.json"), "utf8"));
5836
+ const journal = JSON.parse(await readFile14(join15(root, transactionId, "journal.json"), "utf8"));
5544
5837
  transactions.push({
5545
5838
  transactionId,
5546
5839
  kind: journal.kind,
@@ -5557,7 +5850,7 @@ async function pendingTransactions(projectRoot) {
5557
5850
  }
5558
5851
  async function pathExists2(path) {
5559
5852
  try {
5560
- await stat3(path);
5853
+ await stat4(path);
5561
5854
  return true;
5562
5855
  } catch (error) {
5563
5856
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -5571,11 +5864,11 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
5571
5864
  if (latest === void 0) {
5572
5865
  throw new Error("no committed update transaction is available for rollback");
5573
5866
  }
5574
- const transactionRoot = join14(stateLayout(projectRoot).transactions, latest.transactionId);
5575
- const journal = JSON.parse(await readFile13(join14(transactionRoot, "journal.json"), "utf8"));
5576
- const after = JSON.parse(await readFile13(join14(transactionRoot, "after", "manifest.json"), "utf8"));
5867
+ const transactionRoot = join15(stateLayout(projectRoot).transactions, latest.transactionId);
5868
+ const journal = JSON.parse(await readFile14(join15(transactionRoot, "journal.json"), "utf8"));
5869
+ const after = JSON.parse(await readFile14(join15(transactionRoot, "after", "manifest.json"), "utf8"));
5577
5870
  for (const entry of after) {
5578
- const target = join14(projectRoot, entry.path);
5871
+ const target = join15(projectRoot, entry.path);
5579
5872
  const exists6 = await pathExists2(target);
5580
5873
  if (exists6 !== entry.exists || exists6 && await sha256File(target) !== entry.hash) {
5581
5874
  throw new Error("cannot rollback dirty path: " + entry.path);
@@ -5588,10 +5881,10 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
5588
5881
  continue;
5589
5882
  }
5590
5883
  seen.add(snapshot.path);
5591
- const target = join14(projectRoot, snapshot.path);
5884
+ const target = join15(projectRoot, snapshot.path);
5592
5885
  const exists6 = await pathExists2(target);
5593
5886
  if (snapshot.existed && snapshot.snapshot_name !== null) {
5594
- const content = await readFile13(join14(transactionRoot, "before", snapshot.snapshot_name));
5887
+ const content = await readFile14(join15(transactionRoot, "before", snapshot.snapshot_name));
5595
5888
  operations.push({
5596
5889
  operation: exists6 ? "modify" : "add",
5597
5890
  path: snapshot.path,
@@ -5620,7 +5913,7 @@ async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Dat
5620
5913
  if (!removable) {
5621
5914
  continue;
5622
5915
  }
5623
- await rm7(join14(stateLayout(projectRoot).transactions, item2.transactionId), {
5916
+ await rm7(join15(stateLayout(projectRoot).transactions, item2.transactionId), {
5624
5917
  recursive: true,
5625
5918
  force: true
5626
5919
  });
@@ -5630,8 +5923,8 @@ async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Dat
5630
5923
  }
5631
5924
 
5632
5925
  // ../core/dist/update/update.js
5633
- import { readFile as readFile14 } from "node:fs/promises";
5634
- import { join as join15, resolve as resolve9 } from "node:path";
5926
+ import { readFile as readFile15 } from "node:fs/promises";
5927
+ import { join as join16, resolve as resolve10 } from "node:path";
5635
5928
  import { parse as parseYaml8 } from "yaml";
5636
5929
  var UpdateWorkflowError = class extends Error {
5637
5930
  exitCode;
@@ -5644,10 +5937,10 @@ var UpdateWorkflowError = class extends Error {
5644
5937
  }
5645
5938
  };
5646
5939
  async function updateProject(options) {
5647
- const root = resolve9(options.projectRoot);
5940
+ const root = resolve10(options.projectRoot);
5648
5941
  let project;
5649
5942
  try {
5650
- project = projectConfigSchema.parse(parseYaml8(await readFile14(join15(root, ".harness", "project.yaml"), "utf8")));
5943
+ project = projectConfigSchema.parse(parseYaml8(await readFile15(join16(root, ".harness", "project.yaml"), "utf8")));
5651
5944
  } catch {
5652
5945
  throw new UpdateWorkflowError("project configuration is invalid", 3, "PROJECT_CONFIG_INVALID");
5653
5946
  }
@@ -5671,6 +5964,7 @@ async function updateProject(options) {
5671
5964
  }
5672
5965
  const requestId = uuidV7();
5673
5966
  const baseline = await readBaseline(root);
5967
+ const protocolOnlyPaths = await readManagedProjectRuleProjectionPaths(root);
5674
5968
  let parsedServerUrl;
5675
5969
  try {
5676
5970
  parsedServerUrl = new URL(serverUrl);
@@ -5692,6 +5986,7 @@ async function updateProject(options) {
5692
5986
  client,
5693
5987
  requestId,
5694
5988
  dryRun: options.dryRun,
5989
+ protocolOnlyPaths,
5695
5990
  ...options.conflictStrategy === void 0 ? {} : { conflictStrategy: options.conflictStrategy },
5696
5991
  ...options.resolveOverrides === void 0 ? {} : { resolveOverrides: options.resolveOverrides },
5697
5992
  ...options.confirmConflictStrategy === void 0 ? {} : { confirmConflictStrategy: options.confirmConflictStrategy },
@@ -5702,7 +5997,13 @@ async function updateProject(options) {
5702
5997
  const parsed = harnessAgentSchema.safeParse(agent);
5703
5998
  return parsed.success ? [parsed.data] : [];
5704
5999
  });
5705
- await synchronizeProjectRules(root, agents, project.adapter_options?.codebuddy?.surface ?? "both");
6000
+ const projections = await synchronizeProjectRules(root, agents, project.adapter_options?.codebuddy?.surface ?? "both");
6001
+ for (const path of projections.conflicts) {
6002
+ result.conflicts.push({ path, operation: "modify", reason: "local-dirty" });
6003
+ if (!result.skipped.some((item2) => item2.path === path)) {
6004
+ result.skipped.push({ path, operation: "modify", reason: "local-dirty" });
6005
+ }
6006
+ }
5706
6007
  }
5707
6008
  return result;
5708
6009
  } catch (error) {
@@ -5729,8 +6030,8 @@ async function updateProject(options) {
5729
6030
  }
5730
6031
 
5731
6032
  // src/config/init-config.ts
5732
- import { isAbsolute as isAbsolute2, join as join16 } from "node:path";
5733
- import { readFile as readFile15 } from "node:fs/promises";
6033
+ import { isAbsolute as isAbsolute2, join as join17 } from "node:path";
6034
+ import { readFile as readFile16 } from "node:fs/promises";
5734
6035
  var InitConfigurationError = class extends Error {
5735
6036
  exitCode;
5736
6037
  code;
@@ -5813,9 +6114,9 @@ function hasOwn(record, key) {
5813
6114
  async function resolveInitConfig(cwd, flags, prompts = {}, warnings = []) {
5814
6115
  let fileConfig = {};
5815
6116
  if (flags.config !== void 0) {
5816
- const path = isAbsolute2(flags.config) ? flags.config : join16(cwd, flags.config);
6117
+ const path = isAbsolute2(flags.config) ? flags.config : join17(cwd, flags.config);
5817
6118
  try {
5818
- fileConfig = JSON.parse(await readFile15(path, "utf8"));
6119
+ fileConfig = JSON.parse(await readFile16(path, "utf8"));
5819
6120
  } catch (error) {
5820
6121
  throw new InitConfigurationError(
5821
6122
  "unable to read init config: " + (error instanceof Error ? error.message : String(error)),
@@ -5897,8 +6198,8 @@ function serializeCliResult(result) {
5897
6198
  }
5898
6199
 
5899
6200
  // src/config/codebuddy-setup.ts
5900
- import { mkdir as mkdir5, readFile as readFile16, readdir as readdir8, stat as stat4, writeFile as writeFile7 } from "node:fs/promises";
5901
- import { basename as basename3, dirname as dirname5, extname as extname2, join as join17 } from "node:path";
6201
+ import { mkdir as mkdir6, readFile as readFile17, readdir as readdir9, stat as stat5, writeFile as writeFile8 } from "node:fs/promises";
6202
+ import { basename as basename4, dirname as dirname6, extname as extname2, join as join18 } from "node:path";
5902
6203
  var MANAGED_RULE_NAMES = /* @__PURE__ */ new Set([
5903
6204
  "harness-general.md",
5904
6205
  "harness-general.mdc",
@@ -5909,7 +6210,7 @@ var SENSITIVE_ASSIGNMENT = /(?:password|passwd|token|secret|access[_-]?key|priva
5909
6210
  var PRIVATE_KEY_BLOCK = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i;
5910
6211
  async function exists4(path) {
5911
6212
  try {
5912
- await stat4(path);
6213
+ await stat5(path);
5913
6214
  return true;
5914
6215
  } catch {
5915
6216
  return false;
@@ -5917,7 +6218,7 @@ async function exists4(path) {
5917
6218
  }
5918
6219
  async function readJsonObject(path) {
5919
6220
  try {
5920
- const parsed = JSON.parse(await readFile16(path, "utf8"));
6221
+ const parsed = JSON.parse(await readFile17(path, "utf8"));
5921
6222
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
5922
6223
  } catch (error) {
5923
6224
  if (error instanceof Error && "code" in error && error.code === "ENOENT") return {};
@@ -5925,10 +6226,10 @@ async function readJsonObject(path) {
5925
6226
  }
5926
6227
  }
5927
6228
  async function inspectCodeBuddySetup(projectRoot, surface = "both") {
5928
- const rulesRoot = join17(projectRoot, ".claude", "rules");
6229
+ const rulesRoot = join18(projectRoot, ".claude", "rules");
5929
6230
  let ruleNames = [];
5930
6231
  try {
5931
- ruleNames = (await readdir8(rulesRoot, { withFileTypes: true })).filter((entry) => entry.isFile() && [".md", ".mdc"].includes(extname2(entry.name).toLowerCase())).map((entry) => entry.name).filter((name) => !MANAGED_RULE_NAMES.has(name)).sort();
6232
+ ruleNames = (await readdir9(rulesRoot, { withFileTypes: true })).filter((entry) => entry.isFile() && [".md", ".mdc"].includes(extname2(entry.name).toLowerCase())).map((entry) => entry.name).filter((name) => !MANAGED_RULE_NAMES.has(name)).sort();
5932
6233
  } catch (error) {
5933
6234
  if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
5934
6235
  }
@@ -5936,11 +6237,11 @@ async function inspectCodeBuddySetup(projectRoot, surface = "both") {
5936
6237
  const currentClaudeRules = [];
5937
6238
  const conflictingClaudeRules = [];
5938
6239
  for (const name of ruleNames) {
5939
- const sourceContent = await readFile16(join17(rulesRoot, name), "utf8");
6240
+ const sourceContent = await readFile17(join18(rulesRoot, name), "utf8");
5940
6241
  const targetContents = await Promise.all(
5941
6242
  destinationTargets(projectRoot, surface, name).map(async (target) => {
5942
6243
  try {
5943
- return await readFile16(target, "utf8");
6244
+ return await readFile17(target, "utf8");
5944
6245
  } catch (error) {
5945
6246
  if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
5946
6247
  throw error;
@@ -5954,22 +6255,22 @@ async function inspectCodeBuddySetup(projectRoot, surface = "both") {
5954
6255
  currentClaudeRules.push(name);
5955
6256
  }
5956
6257
  }
5957
- const mcp = await readJsonObject(join17(projectRoot, ".mcp.json"));
6258
+ const mcp = await readJsonObject(join18(projectRoot, ".mcp.json"));
5958
6259
  const servers = mcp?.mcpServers;
5959
6260
  const configured = servers !== null && typeof servers === "object" && !Array.isArray(servers) && Object.prototype.hasOwnProperty.call(servers, "codegraph");
5960
6261
  return {
5961
6262
  claudeRules,
5962
6263
  currentClaudeRules,
5963
6264
  conflictingClaudeRules,
5964
- hasCodeGraphIndex: await exists4(join17(projectRoot, ".codegraph")),
6265
+ hasCodeGraphIndex: await exists4(join18(projectRoot, ".codegraph")),
5965
6266
  codeGraphConfigured: configured
5966
6267
  };
5967
6268
  }
5968
6269
  function destinationTargets(root, surface, name) {
5969
- const stem = basename3(name, extname2(name));
6270
+ const stem = basename4(name, extname2(name));
5970
6271
  const targets = [];
5971
- if (surface !== "cli") targets.push(join17(root, ".codebuddy", ".rules", `${stem}.mdc`));
5972
- if (surface !== "ide") targets.push(join17(root, ".codebuddy", "rules", `${stem}.md`));
6272
+ if (surface !== "cli") targets.push(join18(root, ".codebuddy", ".rules", `${stem}.mdc`));
6273
+ if (surface !== "ide") targets.push(join18(root, ".codebuddy", "rules", `${stem}.md`));
5973
6274
  return targets;
5974
6275
  }
5975
6276
  async function applyCodeBuddySetup(options) {
@@ -5983,8 +6284,8 @@ async function applyCodeBuddySetup(options) {
5983
6284
  if (options.syncClaudeRules) {
5984
6285
  const plan = await inspectCodeBuddySetup(options.projectRoot, options.surface);
5985
6286
  for (const name of plan.claudeRules) {
5986
- const source = join17(options.projectRoot, ".claude", "rules", name);
5987
- const content = await readFile16(source, "utf8");
6287
+ const source = join18(options.projectRoot, ".claude", "rules", name);
6288
+ const content = await readFile17(source, "utf8");
5988
6289
  if (SENSITIVE_ASSIGNMENT.test(content) || PRIVATE_KEY_BLOCK.test(content)) {
5989
6290
  result.skippedSensitive.push(name);
5990
6291
  continue;
@@ -5994,14 +6295,14 @@ async function applyCodeBuddySetup(options) {
5994
6295
  result.preserved.push(target);
5995
6296
  continue;
5996
6297
  }
5997
- await mkdir5(dirname5(target), { recursive: true });
5998
- await writeFile7(target, content, { encoding: "utf8", flag: "wx" });
6298
+ await mkdir6(dirname6(target), { recursive: true });
6299
+ await writeFile8(target, content, { encoding: "utf8", flag: "wx" });
5999
6300
  result.copied.push(target);
6000
6301
  }
6001
6302
  }
6002
6303
  }
6003
6304
  if (options.configureCodeGraph) {
6004
- const path = join17(options.projectRoot, ".mcp.json");
6305
+ const path = join18(options.projectRoot, ".mcp.json");
6005
6306
  const current = await readJsonObject(path);
6006
6307
  if (current === null) {
6007
6308
  result.warnings.push(".mcp.json \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u5DF2\u4FDD\u7559\u539F\u6587\u4EF6\u5E76\u8DF3\u8FC7 CodeGraph MCP \u914D\u7F6E");
@@ -6016,7 +6317,7 @@ async function applyCodeBuddySetup(options) {
6016
6317
  codegraph: { command: "codegraph", args: ["serve", "--mcp"] }
6017
6318
  }
6018
6319
  };
6019
- await writeFile7(path, JSON.stringify(next, null, 2) + "\n", "utf8");
6320
+ await writeFile8(path, JSON.stringify(next, null, 2) + "\n", "utf8");
6020
6321
  result.mcpUpdated = true;
6021
6322
  }
6022
6323
  }
@@ -6025,13 +6326,13 @@ async function applyCodeBuddySetup(options) {
6025
6326
  }
6026
6327
 
6027
6328
  // src/commands/refresh.ts
6028
- import { readFile as readFile17 } from "node:fs/promises";
6029
- import { join as join18 } from "node:path";
6329
+ import { readFile as readFile18 } from "node:fs/promises";
6330
+ import { join as join19 } from "node:path";
6030
6331
  import { parse as parseYaml9 } from "yaml";
6031
6332
  async function detectProject(root) {
6032
6333
  let content;
6033
6334
  try {
6034
- content = await readFile17(join18(root, ".harness", "project.yaml"), "utf8");
6335
+ content = await readFile18(join19(root, ".harness", "project.yaml"), "utf8");
6035
6336
  } catch (error) {
6036
6337
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
6037
6338
  return { status: "absent" };
@@ -6863,12 +7164,94 @@ async function runUpdate(options, dependencies) {
6863
7164
  }
6864
7165
  }
6865
7166
 
7167
+ // src/commands/rules-sync.ts
7168
+ function configuredAgents(config) {
7169
+ const agents = config.adapters.enabled.flatMap((value) => {
7170
+ const parsed = harnessAgentSchema.safeParse(value);
7171
+ return parsed.success ? [parsed.data] : [];
7172
+ });
7173
+ return sortHarnessAgents(agents.length > 0 ? agents : ["claude-code"]);
7174
+ }
7175
+ function configuredSurface(config, override) {
7176
+ const value = override ?? config.adapter_options?.codebuddy?.surface ?? "both";
7177
+ if (value === "both" || value === "ide" || value === "cli") return value;
7178
+ throw new Error("codebuddy surface \u5FC5\u987B\u4E3A both\u3001ide \u6216 cli");
7179
+ }
7180
+ async function runRulesSync(options, dependencies) {
7181
+ const detection = await detectProject(dependencies.cwd);
7182
+ if (detection.status === "absent") {
7183
+ dependencies.stderr("\u5C1A\u672A\u521D\u59CB\u5316 Hunter Harness\uFF1B\u8BF7\u5148\u8FD0\u884C `hunter-harness`\u3002\n");
7184
+ return 3;
7185
+ }
7186
+ if (detection.status === "invalid") {
7187
+ dependencies.stderr("PROJECT_CONFIG_INVALID\uFF1A.harness/project.yaml \u65E0\u6548\n");
7188
+ return 3;
7189
+ }
7190
+ try {
7191
+ const agents = options.agents === void 0 ? configuredAgents(detection.config) : parseAgentsInput(options.agents);
7192
+ const projections = await synchronizeProjectRules(
7193
+ dependencies.cwd,
7194
+ agents,
7195
+ configuredSurface(detection.config, options.codebuddySurface)
7196
+ );
7197
+ const learning = options.learn === false ? null : await synchronizeRuleCandidates(dependencies.cwd);
7198
+ const exitCode = projections.conflicts.length > 0 ? 5 : 0;
7199
+ const payload = {
7200
+ schema_version: 1,
7201
+ command: "rules-sync",
7202
+ request_id: uuidV7(),
7203
+ dry_run: false,
7204
+ ok: exitCode === 0,
7205
+ exit_code: exitCode,
7206
+ project_id: detection.config.project.project_id,
7207
+ summary: {
7208
+ migrated: projections.migrated.length,
7209
+ projected: projections.written.length,
7210
+ removed: projections.removed.length,
7211
+ unchanged: projections.unchanged.length,
7212
+ conflicts: projections.conflicts.length,
7213
+ agent_specific: projections.agent_specific.length,
7214
+ rule_candidates: learning?.candidates ?? 0
7215
+ },
7216
+ items: [
7217
+ ...projections.migrated.map((path) => ({ path, status: "migrated" })),
7218
+ ...projections.written.map((path) => ({ path, status: "projected" })),
7219
+ ...projections.agent_specific.map((path) => ({ path, status: "agent-specific" })),
7220
+ ...learning === null ? [] : [{
7221
+ path: learning.path,
7222
+ status: learning.changed ? "updated" : "unchanged",
7223
+ candidates: learning.candidates,
7224
+ scanned: learning.scanned
7225
+ }]
7226
+ ],
7227
+ warnings: [
7228
+ ...projections.conflicts.map((path) => `\u89C4\u5219\u5206\u6B67\u672A\u8986\u76D6\uFF1A${path}`),
7229
+ ...projections.agent_specific.map((path) => `\u4FDD\u7559 Agent \u4E13\u5C5E\u89C4\u5219\uFF1A${path}`)
7230
+ ],
7231
+ errors: []
7232
+ };
7233
+ if (options.json === true) {
7234
+ dependencies.stdout(serializeCliResult(payload));
7235
+ } else {
7236
+ dependencies.stdout(
7237
+ `\u89C4\u5219\u540C\u6B65\uFF1A\u8FC1\u79FB ${payload.summary.migrated}\uFF0C\u6295\u5F71 ${payload.summary.projected}\uFF0C\u51B2\u7A81 ${payload.summary.conflicts}\uFF0C\u5019\u9009 ${payload.summary.rule_candidates}\u3002
7238
+ `
7239
+ );
7240
+ for (const warning of payload.warnings) dependencies.stderr(warning + "\n");
7241
+ }
7242
+ return exitCode;
7243
+ } catch (error) {
7244
+ dependencies.stderr((error instanceof Error ? error.message : String(error)) + "\n");
7245
+ return 1;
7246
+ }
7247
+ }
7248
+
6866
7249
  // src/commands/recovery.ts
6867
- import { stat as stat5 } from "node:fs/promises";
6868
- import { join as join19 } from "node:path";
7250
+ import { stat as stat6 } from "node:fs/promises";
7251
+ import { join as join20 } from "node:path";
6869
7252
  async function exists5(path) {
6870
7253
  try {
6871
- await stat5(path);
7254
+ await stat6(path);
6872
7255
  return true;
6873
7256
  } catch {
6874
7257
  return false;
@@ -6925,7 +7308,7 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
6925
7308
  return 5;
6926
7309
  }
6927
7310
  }
6928
- const initialized = await exists5(join19(dependencies.cwd, ".harness", "project.yaml"));
7311
+ const initialized = await exists5(join20(dependencies.cwd, ".harness", "project.yaml"));
6929
7312
  if (!initialized || options.nonInteractive === true || explicitConfigure(options)) {
6930
7313
  return null;
6931
7314
  }
@@ -6968,8 +7351,8 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
6968
7351
  }
6969
7352
 
6970
7353
  // src/workflow-data/resolve.ts
6971
- import { cp, mkdir as mkdir6, readdir as readdir9, readFile as readFile18, rm as rm8, stat as stat6 } from "node:fs/promises";
6972
- import { dirname as dirname6, join as join20, relative as relative2 } from "node:path";
7354
+ import { cp, mkdir as mkdir7, readdir as readdir10, readFile as readFile19, rm as rm8, stat as stat7 } from "node:fs/promises";
7355
+ import { dirname as dirname7, join as join21, relative as relative3 } from "node:path";
6973
7356
  import { fileURLToPath } from "node:url";
6974
7357
  var WorkflowDataResolutionError = class extends Error {
6975
7358
  code;
@@ -6983,7 +7366,7 @@ var WorkflowDataResolutionError = class extends Error {
6983
7366
  };
6984
7367
  async function pathExists3(path) {
6985
7368
  try {
6986
- await stat6(path);
7369
+ await stat7(path);
6987
7370
  return true;
6988
7371
  } catch {
6989
7372
  return false;
@@ -7003,17 +7386,17 @@ function workflowPackageName(family, env) {
7003
7386
  return `${scope}/workflow-${family}`;
7004
7387
  }
7005
7388
  async function listFilesRecursive(root, base = root) {
7006
- const entries = await readdir9(root, { withFileTypes: true });
7389
+ const entries = await readdir10(root, { withFileTypes: true });
7007
7390
  const files = [];
7008
7391
  for (const entry of entries) {
7009
- const absolute = join20(root, entry.name);
7392
+ const absolute = join21(root, entry.name);
7010
7393
  if (entry.isDirectory()) {
7011
7394
  files.push(...await listFilesRecursive(absolute, base));
7012
7395
  continue;
7013
7396
  }
7014
7397
  if (!entry.isFile()) continue;
7015
- const rel = relative2(base, absolute).replaceAll("\\", "/");
7016
- files.push({ path: rel, content: await readFile18(absolute, "utf8") });
7398
+ const rel = relative3(base, absolute).replaceAll("\\", "/");
7399
+ files.push({ path: rel, content: await readFile19(absolute, "utf8") });
7017
7400
  }
7018
7401
  return files;
7019
7402
  }
@@ -7026,7 +7409,7 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
7026
7409
  const manifest = await readWorkflowFamilyManifest(resourcesRoot);
7027
7410
  const expected = manifest.content_sha256;
7028
7411
  if (typeof expected !== "string" || expected.length === 0) return;
7029
- const harnessRoot = join20(resourcesRoot, "harness");
7412
+ const harnessRoot = join21(resourcesRoot, "harness");
7030
7413
  if (!await pathExists3(harnessRoot)) {
7031
7414
  throw new WorkflowDataResolutionError(
7032
7415
  "\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u7F3A\u5C11 harness/\uFF0C\u65E0\u6CD5\u6821\u9A8C SHA-256",
@@ -7045,40 +7428,40 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
7045
7428
  }
7046
7429
  }
7047
7430
  async function monorepoResourcesRoot() {
7048
- const here = dirname6(fileURLToPath(import.meta.url));
7431
+ const here = dirname7(fileURLToPath(import.meta.url));
7049
7432
  const candidates = [
7050
7433
  // TypeScript source: packages/cli/src/workflow-data -> packages/workflow-data-harness.
7051
- join20(here, "../../../workflow-data-harness"),
7434
+ join21(here, "../../../workflow-data-harness"),
7052
7435
  // Bundled CLI: packages/cli/dist -> packages/workflow-data-harness.
7053
- join20(here, "../../workflow-data-harness"),
7436
+ join21(here, "../../workflow-data-harness"),
7054
7437
  // Test/build layouts that preserve additional source directory levels.
7055
- join20(here, "../../../../packages/workflow-data-harness")
7438
+ join21(here, "../../../../packages/workflow-data-harness")
7056
7439
  ];
7057
7440
  for (const candidate of candidates) {
7058
- if (await pathExists3(join20(candidate, "harness", "manifests"))) return candidate;
7441
+ if (await pathExists3(join21(candidate, "harness", "manifests"))) return candidate;
7059
7442
  }
7060
7443
  return null;
7061
7444
  }
7062
7445
  async function siblingWorkflowPackage(cwd) {
7063
- const here = dirname6(fileURLToPath(import.meta.url));
7446
+ const here = dirname7(fileURLToPath(import.meta.url));
7064
7447
  const candidates = [
7065
- join20(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
7448
+ join21(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
7066
7449
  // Published layout: node_modules/hunter-harness/dist/bin.js alongside the
7067
7450
  // scoped workflow package in node_modules/@hunter-harness/workflow-harness.
7068
- join20(here, "..", "..", "@hunter-harness", "workflow-harness"),
7451
+ join21(here, "..", "..", "@hunter-harness", "workflow-harness"),
7069
7452
  // Monorepo bundled layout: packages/cli/dist/bin.js.
7070
- join20(here, "..", "..", "workflow-data-harness")
7453
+ join21(here, "..", "..", "workflow-data-harness")
7071
7454
  ];
7072
7455
  for (const candidate of candidates) {
7073
- if (await pathExists3(join20(candidate, "harness", "manifests"))) return candidate;
7456
+ if (await pathExists3(join21(candidate, "harness", "manifests"))) return candidate;
7074
7457
  }
7075
7458
  return null;
7076
7459
  }
7077
7460
  async function readCachedNpmPackageVersion(cacheRoot) {
7078
- const manifestPath = join20(cacheRoot, "package.json");
7461
+ const manifestPath = join21(cacheRoot, "package.json");
7079
7462
  if (!await pathExists3(manifestPath)) return null;
7080
7463
  try {
7081
- const pkg = JSON.parse(await readFile18(manifestPath, "utf8"));
7464
+ const pkg = JSON.parse(await readFile19(manifestPath, "utf8"));
7082
7465
  return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : null;
7083
7466
  } catch {
7084
7467
  return null;
@@ -7100,13 +7483,13 @@ async function latestWorkflowCacheIsStale(cacheRoot, packageName, resolveManifes
7100
7483
  }
7101
7484
  }
7102
7485
  async function materializeWorkflowPackageCache(cacheRoot, packageSpec, extract) {
7103
- await mkdir6(cacheRoot, { recursive: true });
7104
- const staging = join20(cacheRoot, ".extract");
7486
+ await mkdir7(cacheRoot, { recursive: true });
7487
+ const staging = join21(cacheRoot, ".extract");
7105
7488
  await rm8(staging, { recursive: true, force: true });
7106
- await mkdir6(staging, { recursive: true });
7489
+ await mkdir7(staging, { recursive: true });
7107
7490
  await extract(packageSpec, staging);
7108
- const packageDir = await pathExists3(join20(staging, "harness", "manifests")) ? staging : join20(staging, "package");
7109
- if (!await pathExists3(join20(packageDir, "harness", "manifests"))) {
7491
+ const packageDir = await pathExists3(join21(staging, "harness", "manifests")) ? staging : join21(staging, "package");
7492
+ if (!await pathExists3(join21(packageDir, "harness", "manifests"))) {
7110
7493
  throw new WorkflowDataResolutionError(
7111
7494
  "\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u5185\u5BB9\u65E0\u6548\uFF1A\u7F3A\u5C11 harness/manifests",
7112
7495
  "WORKFLOW_DATA_INVALID",
@@ -7135,8 +7518,8 @@ async function resolveWorkflowResourcesRoot(options, argv = []) {
7135
7518
  const packageName = workflowPackageName(family, options.env);
7136
7519
  const packageSpec = version === "latest" ? packageName : `${packageName}@${version}`;
7137
7520
  const cacheKey = packageSpec.replace("/", "+");
7138
- const cacheRoot = join20(options.cwd, ".harness", "cache", "workflow-packages", cacheKey);
7139
- if (await pathExists3(join20(cacheRoot, "harness", "manifests"))) {
7521
+ const cacheRoot = join21(options.cwd, ".harness", "cache", "workflow-packages", cacheKey);
7522
+ if (await pathExists3(join21(cacheRoot, "harness", "manifests"))) {
7140
7523
  if (version === "latest") {
7141
7524
  const stale = await latestWorkflowCacheIsStale(cacheRoot, packageName, options.pacoteManifest);
7142
7525
  if (stale) {
@@ -7182,9 +7565,9 @@ function describeWorkflowDataFetchFailure(error, packageSpec) {
7182
7565
  return `\u65E0\u6CD5\u83B7\u53D6\u5DE5\u4F5C\u6D41\u6570\u636E\u5305 ${packageSpec}\uFF1A\u672C\u5730\u7F13\u5B58\u4E0D\u5B58\u5728\u4E14\u83B7\u53D6\u5931\u8D25\u3002\u53EF${hintRoot}\u3002\u539F\u56E0\uFF1A${code !== "" ? code + " " : ""}${detail}`;
7183
7566
  }
7184
7567
  async function readWorkflowFamilyManifest(resourcesRoot) {
7185
- const manifestPath = join20(resourcesRoot, "hunter-workflow-family.json");
7568
+ const manifestPath = join21(resourcesRoot, "hunter-workflow-family.json");
7186
7569
  if (!await pathExists3(manifestPath)) return {};
7187
- return JSON.parse(await readFile18(manifestPath, "utf8"));
7570
+ return JSON.parse(await readFile19(manifestPath, "utf8"));
7188
7571
  }
7189
7572
 
7190
7573
  // src/bin.ts
@@ -7298,6 +7681,12 @@ async function runCli(argv, overrides = {}) {
7298
7681
  dependencies
7299
7682
  );
7300
7683
  });
7684
+ program.command("rules-sync").description("\u6536\u655B\u516C\u5171\u89C4\u5219\u3001\u5237\u65B0 Agent \u6295\u5F71\u5E76\u63D0\u70BC\u5386\u53F2\u89C4\u5219\u5019\u9009").option("--agents <csv>").option("--codebuddy-surface <surface>").option("--no-learn", "\u53EA\u540C\u6B65\u89C4\u5219\uFF0C\u4E0D\u8BFB\u53D6\u5386\u53F2 review/test \u8BC1\u636E").option("--json").action(async (options) => {
7685
+ exitCode = await runRulesSync(
7686
+ { ...program.opts(), ...options },
7687
+ dependencies
7688
+ );
7689
+ });
7301
7690
  try {
7302
7691
  await program.parseAsync(["node", "hunter-harness", ...argv]);
7303
7692
  return exitCode;