hunter-harness 0.2.27 → 0.2.29

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 +790 -204
  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((resolve9) => setTimeout(resolve9, 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) {
@@ -1684,6 +1684,9 @@ function classifyFile(input) {
1684
1684
  if (under(path, ".harness/knowledge/")) {
1685
1685
  return USER_DIFF;
1686
1686
  }
1687
+ if (under(path, ".harness/rules/")) {
1688
+ return USER_DIFF;
1689
+ }
1687
1690
  if (/^\.harness\/archive\/[^/]+\/reports\/final\/summary-data\.json$/u.test(path)) {
1688
1691
  return GENERATED_REVIEWABLE;
1689
1692
  }
@@ -1696,7 +1699,7 @@ function classifyFile(input) {
1696
1699
  if (under(path, ".harness/reports/")) {
1697
1700
  return REPORT_CACHE;
1698
1701
  }
1699
- if (under(path, ".harness/state/") || under(path, ".harness/rules/")) {
1702
+ if (under(path, ".harness/state/")) {
1700
1703
  return INTERNAL_STATE;
1701
1704
  }
1702
1705
  if (under(path, ".harness/generated/") || under(path, ".harness/cache/")) {
@@ -1730,9 +1733,9 @@ function decideUpdate(policy, dirty) {
1730
1733
  }
1731
1734
 
1732
1735
  // ../core/dist/project/initialize.js
1733
- import { createHash as createHash4 } from "node:crypto";
1734
- import { readFile as readFile4, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
1735
- import { basename, join as join5, resolve as resolve4 } from "node:path";
1736
+ import { createHash as createHash5 } from "node:crypto";
1737
+ import { readFile as readFile5, stat as stat2, writeFile as writeFile4 } from "node:fs/promises";
1738
+ import { basename as basename2, join as join6, resolve as resolve5 } from "node:path";
1736
1739
  import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
1737
1740
 
1738
1741
  // ../core/dist/transaction/transaction.js
@@ -2247,10 +2250,244 @@ function managedTargetsFor(adapter, bundle, context) {
2247
2250
  return records.sort((left, right) => left.target_path.localeCompare(right.target_path));
2248
2251
  }
2249
2252
 
2250
- // ../core/dist/project/profile-bundle.js
2253
+ // ../core/dist/project/project-rules.js
2251
2254
  import { createHash as createHash3 } from "node:crypto";
2252
- import { readFile as readFile3, readdir as readdir2 } from "node:fs/promises";
2253
- import { join as join4, resolve as resolve3 } from "node:path";
2255
+ import { mkdir as mkdir4, readFile as readFile3, readdir as readdir2, rename as rename3, unlink, writeFile as writeFile3 } from "node:fs/promises";
2256
+ import { basename, dirname as dirname3, extname, join as join4, resolve as resolve3 } from "node:path";
2257
+ var RULES_ROOT = ".harness/rules";
2258
+ var RECEIPT_PATH = ".harness/state/local/rule-projections.json";
2259
+ var CODEX_BLOCK_ID = "hunter-harness-project-rules";
2260
+ var MANAGED_NAMES = /* @__PURE__ */ new Set([
2261
+ "harness-general.md",
2262
+ "harness-general.mdc",
2263
+ "harness-profile-java.md",
2264
+ "harness-profile-java.mdc"
2265
+ ]);
2266
+ var AGENT_RULE_ROOTS = [
2267
+ ".claude/rules",
2268
+ ".cursor/rules",
2269
+ ".codebuddy/.rules",
2270
+ ".codebuddy/rules"
2271
+ ];
2272
+ function sha256(content) {
2273
+ return createHash3("sha256").update(content).digest("hex");
2274
+ }
2275
+ async function optionalText(path) {
2276
+ try {
2277
+ return await readFile3(path, "utf8");
2278
+ } catch (error) {
2279
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
2280
+ return null;
2281
+ throw error;
2282
+ }
2283
+ }
2284
+ async function readReceipt(root) {
2285
+ try {
2286
+ const parsed = JSON.parse(await readFile3(join4(root, RECEIPT_PATH), "utf8"));
2287
+ if (parsed.schema_version === 1 && parsed.targets && parsed.source_hashes)
2288
+ return parsed;
2289
+ } catch {
2290
+ }
2291
+ return { schema_version: 1, source_hashes: {}, targets: {} };
2292
+ }
2293
+ async function markdownFiles(root) {
2294
+ try {
2295
+ 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();
2296
+ } catch (error) {
2297
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
2298
+ return [];
2299
+ throw error;
2300
+ }
2301
+ }
2302
+ function portable(path) {
2303
+ return path.replaceAll("\\", "/");
2304
+ }
2305
+ function normalizeRuleContent(content) {
2306
+ return content.replace(/\r\n/g, "\n").trimEnd() + "\n";
2307
+ }
2308
+ function canonicalImportContent(content) {
2309
+ const normalized = content.replace(/\r\n/g, "\n");
2310
+ if (!normalized.startsWith("---\n"))
2311
+ return normalizeRuleContent(normalized);
2312
+ const closing = normalized.indexOf("\n---\n", 4);
2313
+ if (closing < 0)
2314
+ return null;
2315
+ const frontmatter = normalized.slice(4, closing);
2316
+ if (/^\s*(?:globs?|paths?)\s*:/im.test(frontmatter) || /^\s*alwaysApply\s*:\s*false\s*$/im.test(frontmatter)) {
2317
+ return null;
2318
+ }
2319
+ return normalizeRuleContent(normalized.slice(closing + 5));
2320
+ }
2321
+ async function collectImportCandidates(root, previous, result) {
2322
+ const candidates = [];
2323
+ for (const ruleRoot of AGENT_RULE_ROOTS) {
2324
+ for (const name of await markdownFiles(join4(root, ...ruleRoot.split("/")))) {
2325
+ const source = portable(`${ruleRoot}/${name}`);
2326
+ const content = await readFile3(join4(root, ...source.split("/")), "utf8");
2327
+ if (previous.targets[source] === sha256(content))
2328
+ continue;
2329
+ const canonical = canonicalImportContent(content);
2330
+ if (canonical === null) {
2331
+ result.agent_specific.push(source);
2332
+ continue;
2333
+ }
2334
+ candidates.push({
2335
+ source,
2336
+ destination: `${RULES_ROOT}/${basename(name, extname(name))}.md`,
2337
+ content: canonical
2338
+ });
2339
+ }
2340
+ }
2341
+ return candidates;
2342
+ }
2343
+ async function importAgentRules(root, canonicalRoot, previous, result) {
2344
+ const grouped = /* @__PURE__ */ new Map();
2345
+ for (const candidate of await collectImportCandidates(root, previous, result)) {
2346
+ const values = grouped.get(candidate.destination) ?? [];
2347
+ values.push(candidate);
2348
+ grouped.set(candidate.destination, values);
2349
+ }
2350
+ for (const [destination, candidates] of [...grouped].sort(([a], [b]) => a.localeCompare(b))) {
2351
+ const destinationPath = join4(canonicalRoot, basename(destination));
2352
+ const current = await optionalText(destinationPath);
2353
+ const distinct = new Set(candidates.map((candidate) => candidate.content));
2354
+ const representative = candidates.at(0);
2355
+ if (representative === void 0)
2356
+ continue;
2357
+ if (current === null && distinct.size === 1) {
2358
+ await atomicWrite(destinationPath, representative.content);
2359
+ result.migrated.push(destination);
2360
+ continue;
2361
+ }
2362
+ if (current !== null && distinct.size === 1 && normalizeRuleContent(current) === representative.content) {
2363
+ continue;
2364
+ }
2365
+ result.conflicts.push(...candidates.map((candidate) => candidate.source));
2366
+ }
2367
+ }
2368
+ function targetsFor(name, agents, surface) {
2369
+ const stem = basename(name, extname(name));
2370
+ const targets = [];
2371
+ if (agents.includes("claude-code"))
2372
+ targets.push(`.claude/rules/${stem}.md`);
2373
+ if (agents.includes("cursor"))
2374
+ targets.push(`.cursor/rules/${stem}.mdc`);
2375
+ if (agents.includes("codebuddy") && surface !== "cli") {
2376
+ targets.push(`.codebuddy/.rules/${stem}.mdc`);
2377
+ }
2378
+ if (agents.includes("codebuddy") && surface !== "ide") {
2379
+ targets.push(`.codebuddy/rules/${stem}.md`);
2380
+ }
2381
+ return targets;
2382
+ }
2383
+ async function atomicWrite(path, content) {
2384
+ await mkdir4(dirname3(path), { recursive: true });
2385
+ const temporary = `${path}.${process.pid}.tmp`;
2386
+ await writeFile3(temporary, content, "utf8");
2387
+ await rename3(temporary, path);
2388
+ }
2389
+ async function synchronizeProjectRules(projectRoot, agents, surface = "both") {
2390
+ const root = resolve3(projectRoot);
2391
+ const canonicalRoot = join4(root, RULES_ROOT);
2392
+ const result = {
2393
+ migrated: [],
2394
+ agent_specific: [],
2395
+ written: [],
2396
+ removed: [],
2397
+ unchanged: [],
2398
+ conflicts: []
2399
+ };
2400
+ await mkdir4(canonicalRoot, { recursive: true });
2401
+ const previous = await readReceipt(root);
2402
+ await importAgentRules(root, canonicalRoot, previous, result);
2403
+ const next = { schema_version: 1, source_hashes: {}, targets: {} };
2404
+ const desired = /* @__PURE__ */ new Map();
2405
+ for (const name of await markdownFiles(canonicalRoot)) {
2406
+ const sourcePath = `${RULES_ROOT}/${name}`;
2407
+ const content = await readFile3(join4(canonicalRoot, name), "utf8");
2408
+ next.source_hashes[sourcePath] = sha256(content);
2409
+ for (const target of targetsFor(name, agents, surface))
2410
+ desired.set(target, content);
2411
+ }
2412
+ for (const [target, content] of desired) {
2413
+ const path = join4(root, target);
2414
+ const current = await optionalText(path);
2415
+ const incomingHash = sha256(content);
2416
+ const canonicalCurrent = current === null ? null : canonicalImportContent(current);
2417
+ if (current === content || canonicalCurrent === content) {
2418
+ result.unchanged.push(target);
2419
+ next.targets[target] = current === null ? incomingHash : sha256(current);
2420
+ } else if (current === null || previous.targets[target] === sha256(current)) {
2421
+ await atomicWrite(path, content);
2422
+ result.written.push(target);
2423
+ next.targets[target] = incomingHash;
2424
+ } else {
2425
+ result.conflicts.push(target);
2426
+ next.targets[target] = previous.targets[target] ?? sha256(current);
2427
+ }
2428
+ }
2429
+ for (const [target, trustedHash] of Object.entries(previous.targets)) {
2430
+ if (target === "AGENTS.md")
2431
+ continue;
2432
+ if (desired.has(target))
2433
+ continue;
2434
+ const path = join4(root, target);
2435
+ const current = await optionalText(path);
2436
+ if (current === null)
2437
+ continue;
2438
+ if (sha256(current) === trustedHash) {
2439
+ await unlink(path);
2440
+ result.removed.push(target);
2441
+ } else {
2442
+ result.conflicts.push(target);
2443
+ next.targets[target] = trustedHash;
2444
+ }
2445
+ }
2446
+ if (agents.includes("codex")) {
2447
+ const rules = Object.keys(next.source_hashes).sort();
2448
+ const body = [
2449
+ "Before project work, read and follow these shared project rules:",
2450
+ ...rules.map((path) => `- \`${path}\``)
2451
+ ].join("\n");
2452
+ const agentsPath = join4(root, "AGENTS.md");
2453
+ const current = await optionalText(agentsPath) ?? "";
2454
+ const updated = rules.length > 0 ? upsertManagedBlockById(current, CODEX_BLOCK_ID, body) : removeManagedBlockById(current, CODEX_BLOCK_ID);
2455
+ const target = "AGENTS.md";
2456
+ const semanticallyEqual = updated.replace(/\r\n/g, "\n").trimEnd() === current.replace(/\r\n/g, "\n").trimEnd();
2457
+ if (semanticallyEqual)
2458
+ result.unchanged.push(target);
2459
+ else {
2460
+ await atomicWrite(agentsPath, updated);
2461
+ result.written.push(target);
2462
+ }
2463
+ if (rules.length > 0) {
2464
+ next.targets[target] = sha256(semanticallyEqual ? current : updated);
2465
+ }
2466
+ } else if (Object.prototype.hasOwnProperty.call(previous.targets, "AGENTS.md")) {
2467
+ const agentsPath = join4(root, "AGENTS.md");
2468
+ const current = await optionalText(agentsPath);
2469
+ if (current !== null) {
2470
+ const updated = removeManagedBlockById(current, CODEX_BLOCK_ID);
2471
+ if (updated !== current) {
2472
+ await atomicWrite(agentsPath, updated);
2473
+ result.written.push("AGENTS.md");
2474
+ }
2475
+ }
2476
+ }
2477
+ await atomicWrite(join4(root, RECEIPT_PATH), JSON.stringify(next, null, 2) + "\n");
2478
+ result.migrated.sort();
2479
+ result.agent_specific.sort();
2480
+ result.written.sort();
2481
+ result.removed.sort();
2482
+ result.unchanged.sort();
2483
+ result.conflicts = [...new Set(result.conflicts)].sort();
2484
+ return result;
2485
+ }
2486
+
2487
+ // ../core/dist/project/profile-bundle.js
2488
+ import { createHash as createHash4 } from "node:crypto";
2489
+ import { readFile as readFile4, readdir as readdir3 } from "node:fs/promises";
2490
+ import { join as join5, resolve as resolve4 } from "node:path";
2254
2491
  var AdapterBundleError = class extends Error {
2255
2492
  code;
2256
2493
  exitCode = 7;
@@ -2276,15 +2513,15 @@ function validateRelativeBundlePath2(path) {
2276
2513
  }
2277
2514
  var bundleCache = /* @__PURE__ */ new Map();
2278
2515
  async function loadAgentBundle(resourcesRoot, profile, agent) {
2279
- const cacheKey = `${resolve3(resourcesRoot)}:${profile}:${agent}`;
2516
+ const cacheKey = `${resolve4(resourcesRoot)}:${profile}:${agent}`;
2280
2517
  const cached = bundleCache.get(cacheKey);
2281
2518
  if (cached) {
2282
2519
  return { manifest: cached.manifest, files: new Map(cached.files) };
2283
2520
  }
2284
- const manifestPath = join4(resourcesRoot, "harness", "manifests", profile, `${agent}.json`);
2521
+ const manifestPath = join5(resourcesRoot, "harness", "manifests", profile, `${agent}.json`);
2285
2522
  let manifestText;
2286
2523
  try {
2287
- manifestText = await readFile3(manifestPath, "utf8");
2524
+ manifestText = await readFile4(manifestPath, "utf8");
2288
2525
  } catch (error) {
2289
2526
  if (isEnoent(error)) {
2290
2527
  throw new AdapterBundleError("ADAPTER_BUNDLE_MISSING", `offline Harness Bundle manifest missing: ${profile}/${agent}`);
@@ -2312,14 +2549,14 @@ async function loadAgentBundle(resourcesRoot, profile, agent) {
2312
2549
  }
2313
2550
  let bytes;
2314
2551
  try {
2315
- bytes = await readFile3(join4(resourcesRoot, "harness", "bundles", profile, agent, item2.path));
2552
+ bytes = await readFile4(join5(resourcesRoot, "harness", "bundles", profile, agent, item2.path));
2316
2553
  } catch (error) {
2317
2554
  if (isEnoent(error)) {
2318
2555
  throw new AdapterBundleError("ADAPTER_BUNDLE_MISSING", `offline Harness Bundle file missing: ${profile}/${agent}/${item2.path}`);
2319
2556
  }
2320
2557
  throw error;
2321
2558
  }
2322
- if (createHash3("sha256").update(bytes).digest("hex") !== item2.sha256) {
2559
+ if (createHash4("sha256").update(bytes).digest("hex") !== item2.sha256) {
2323
2560
  throw new AdapterBundleError("ADAPTER_BUNDLE_INVALID", `Harness Bundle hash mismatch: ${profile}/${agent}/${item2.path}`);
2324
2561
  }
2325
2562
  files.set(item2.path, bytes);
@@ -2384,10 +2621,10 @@ function parseMigrationManifest(raw) {
2384
2621
  };
2385
2622
  }
2386
2623
  async function loadMigrationManifests(resourcesRoot) {
2387
- const migrationsRoot = join4(resourcesRoot, "harness", "migrations");
2624
+ const migrationsRoot = join5(resourcesRoot, "harness", "migrations");
2388
2625
  let versionDirs;
2389
2626
  try {
2390
- versionDirs = await readdir2(migrationsRoot);
2627
+ versionDirs = await readdir3(migrationsRoot);
2391
2628
  } catch (error) {
2392
2629
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
2393
2630
  return [];
@@ -2398,7 +2635,7 @@ async function loadMigrationManifests(resourcesRoot) {
2398
2635
  for (const versionDir of versionDirs) {
2399
2636
  let files;
2400
2637
  try {
2401
- files = await readdir2(join4(migrationsRoot, versionDir));
2638
+ files = await readdir3(join5(migrationsRoot, versionDir));
2402
2639
  } catch (error) {
2403
2640
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
2404
2641
  continue;
@@ -2408,7 +2645,7 @@ async function loadMigrationManifests(resourcesRoot) {
2408
2645
  for (const file of files) {
2409
2646
  if (!file.endsWith(".json"))
2410
2647
  continue;
2411
- manifests.push(parseMigrationManifest(JSON.parse(await readFile3(join4(migrationsRoot, versionDir, file), "utf8"))));
2648
+ manifests.push(parseMigrationManifest(JSON.parse(await readFile4(join5(migrationsRoot, versionDir, file), "utf8"))));
2412
2649
  }
2413
2650
  }
2414
2651
  return manifests;
@@ -2440,14 +2677,14 @@ function uuidV7(now = Date.now()) {
2440
2677
  var CONTEXT_INDEX_PATH = ".harness/context-index.json";
2441
2678
  async function fileHex(path) {
2442
2679
  try {
2443
- const data = await readFile4(path);
2444
- return createHash4("sha256").update(data).digest("hex");
2680
+ const data = await readFile5(path);
2681
+ return createHash5("sha256").update(data).digest("hex");
2445
2682
  } catch {
2446
2683
  return null;
2447
2684
  }
2448
2685
  }
2449
2686
  async function readExistingSkillBundles(root) {
2450
- const text = await readOptional(join5(root, CONTEXT_INDEX_PATH));
2687
+ const text = await readOptional(join6(root, CONTEXT_INDEX_PATH));
2451
2688
  if (text === "")
2452
2689
  return /* @__PURE__ */ new Map();
2453
2690
  try {
@@ -2467,7 +2704,7 @@ var TargetCollisionError = class extends Error {
2467
2704
  }
2468
2705
  };
2469
2706
  function hex(bytes) {
2470
- return createHash4("sha256").update(bytes).digest("hex");
2707
+ return createHash5("sha256").update(bytes).digest("hex");
2471
2708
  }
2472
2709
  function bytesEqual(left, right) {
2473
2710
  if (left.byteLength !== right.byteLength)
@@ -2480,7 +2717,7 @@ function bytesEqual(left, right) {
2480
2717
  }
2481
2718
  async function readOptional(path) {
2482
2719
  try {
2483
- return await readFile4(path, "utf8");
2720
+ return await readFile5(path, "utf8");
2484
2721
  } catch (error) {
2485
2722
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
2486
2723
  return "";
@@ -2500,12 +2737,12 @@ async function exists2(path) {
2500
2737
  }
2501
2738
  }
2502
2739
  async function existingProjectConfig(root) {
2503
- const path = join5(root, ".harness", "project.yaml");
2740
+ const path = join6(root, ".harness", "project.yaml");
2504
2741
  const content = await readOptional(path);
2505
2742
  return content === "" ? null : projectConfigSchema.parse(parseYaml2(content));
2506
2743
  }
2507
2744
  async function operationFor(root, path, content) {
2508
- return await exists2(join5(root, path)) ? { operation: "modify", path, content } : { operation: "add", path, content };
2745
+ return await exists2(join6(root, path)) ? { operation: "modify", path, content } : { operation: "add", path, content };
2509
2746
  }
2510
2747
  var INSTALLED_BUNDLE_PATH = ".harness/state/local/installed-harness-bundle.json";
2511
2748
  function mergeOwnedTargets(owned) {
@@ -2541,7 +2778,7 @@ function mergeOwnedTargets(owned) {
2541
2778
  });
2542
2779
  }
2543
2780
  async function initializeProject(options) {
2544
- const root = resolve4(options.projectRoot);
2781
+ const root = resolve5(options.projectRoot);
2545
2782
  const config = initConfigSchema.parse(options.config);
2546
2783
  const existing = await existingProjectConfig(root);
2547
2784
  const profile = config.profile;
@@ -2584,7 +2821,7 @@ async function initializeProject(options) {
2584
2821
  const projectConfig = projectConfigSchema.parse({
2585
2822
  harness: { name: "hunter-harness", schema_version: 1 },
2586
2823
  project: {
2587
- name: existing?.project.name ?? basename(root),
2824
+ name: existing?.project.name ?? basename2(root),
2588
2825
  root: ".",
2589
2826
  local_project_key: existing?.project.local_project_key ?? uuidV7(),
2590
2827
  project_id: config.project_id ?? existing?.project.project_id ?? null,
@@ -2606,7 +2843,7 @@ async function initializeProject(options) {
2606
2843
  files: {}
2607
2844
  });
2608
2845
  const managedBlocks = [];
2609
- let agentsContent = await readOptional(join5(root, "AGENTS.md"));
2846
+ let agentsContent = await readOptional(join6(root, "AGENTS.md"));
2610
2847
  agentsContent = upsertManagedBlockById(agentsContent, AGENTS_CORE_BLOCK_ID, AGENTS_MANAGED_BLOCK_CONTENT);
2611
2848
  managedBlocks.push({
2612
2849
  owner: "shared",
@@ -2643,7 +2880,7 @@ async function initializeProject(options) {
2643
2880
  ["AGENTS.md", agentsContent]
2644
2881
  ]);
2645
2882
  if (enabledAgents.includes("claude-code")) {
2646
- let claudeContent = await readOptional(join5(root, "CLAUDE.md"));
2883
+ let claudeContent = await readOptional(join6(root, "CLAUDE.md"));
2647
2884
  claudeContent = upsertManagedBlockById(claudeContent, CLAUDE_BLOCK_ID, CLAUDE_MANAGED_BLOCK_CONTENT);
2648
2885
  files.set("CLAUDE.md", claudeContent);
2649
2886
  managedBlocks.push({
@@ -2654,7 +2891,7 @@ async function initializeProject(options) {
2654
2891
  });
2655
2892
  }
2656
2893
  if (enabledAgents.includes("codebuddy")) {
2657
- let codebuddyContent = await readOptional(join5(root, "CODEBUDDY.md"));
2894
+ let codebuddyContent = await readOptional(join6(root, "CODEBUDDY.md"));
2658
2895
  codebuddyContent = upsertManagedBlockById(codebuddyContent, CODEBUDDY_BLOCK_ID, CODEBUDDY_MANAGED_BLOCK_CONTENT);
2659
2896
  files.set("CODEBUDDY.md", codebuddyContent);
2660
2897
  managedBlocks.push({
@@ -2691,6 +2928,7 @@ async function initializeProject(options) {
2691
2928
  const writeOperations = await Promise.all([...files.entries()].map(async ([path, content]) => operationFor(root, path, content)));
2692
2929
  await runTransaction(root, writeOperations, { kind: "init" });
2693
2930
  await enrichContextIndexWithVerification(root, enabledAgents, profile, options.resourcesRoot, adapterContext);
2931
+ await synchronizeProjectRules(root, enabledAgents, surface);
2694
2932
  }
2695
2933
  return {
2696
2934
  projectConfig,
@@ -2700,8 +2938,8 @@ async function initializeProject(options) {
2700
2938
  };
2701
2939
  }
2702
2940
  async function enrichContextIndexWithVerification(root, agents, profile, resourcesRoot, adapterContext) {
2703
- const contextPath = join5(root, CONTEXT_INDEX_PATH);
2704
- const existing = await readOptional(join5(contextPath));
2941
+ const contextPath = join6(root, CONTEXT_INDEX_PATH);
2942
+ const existing = await readOptional(join6(contextPath));
2705
2943
  if (existing === "")
2706
2944
  return;
2707
2945
  let parsed;
@@ -2726,7 +2964,7 @@ async function enrichContextIndexWithVerification(root, agents, profile, resourc
2726
2964
  const entries = [];
2727
2965
  for (const target of targets) {
2728
2966
  const rel = target.target_path.replace(/\\/g, "/");
2729
- const actual = await fileHex(join5(root, target.target_path));
2967
+ const actual = await fileHex(join6(root, target.target_path));
2730
2968
  entries.push({ relpath: rel, sha256: actual ?? "" });
2731
2969
  if (actual === null) {
2732
2970
  mismatches.push({ relpath: rel, expected: target.sha256, actual: "<missing>" });
@@ -2743,19 +2981,240 @@ async function enrichContextIndexWithVerification(root, agents, profile, resourc
2743
2981
  entry.verificationStatus = newStatus;
2744
2982
  entry.mismatchDetails = mismatches;
2745
2983
  }
2746
- await writeFile3(contextPath, JSON.stringify(parsed, null, 2) + "\n", "utf8");
2984
+ await writeFile4(contextPath, JSON.stringify(parsed, null, 2) + "\n", "utf8");
2985
+ }
2986
+
2987
+ // ../core/dist/project/rule-candidates.js
2988
+ import { createHash as createHash6 } from "node:crypto";
2989
+ import { mkdir as mkdir5, readFile as readFile6, readdir as readdir4, rename as rename4, stat as stat3, writeFile as writeFile5 } from "node:fs/promises";
2990
+ import { basename as basename3, dirname as dirname4, join as join7, relative, resolve as resolve6 } from "node:path";
2991
+ var ARCHIVE_ROOT = ".harness/archive";
2992
+ var CANDIDATE_PATH = ".harness/knowledge/rule-candidates.json";
2993
+ var MAX_EVIDENCE_BYTES = 2 * 1024 * 1024;
2994
+ var EVIDENCE_NAMES = [
2995
+ /^review-findings.*\.json$/i,
2996
+ /^test-(?:report|results?|failures?).*\.json$/i,
2997
+ /^summary-data\.json$/i
2998
+ ];
2999
+ function sha2562(content) {
3000
+ return createHash6("sha256").update(content).digest("hex");
3001
+ }
3002
+ function portable2(path) {
3003
+ return path.replaceAll("\\", "/");
3004
+ }
3005
+ function normalizeText(value) {
3006
+ return value.replace(/\s+/g, " ").trim();
3007
+ }
3008
+ function safeText(value, limit = 500) {
3009
+ if (typeof value !== "string")
3010
+ return null;
3011
+ const normalized = normalizeText(value).slice(0, limit);
3012
+ if (normalized.length < 8)
3013
+ return null;
3014
+ if (/(?:ignore|disregard)\s+(?:all\s+)?previous|system\s+prompt|developer\s+message/i.test(normalized)) {
3015
+ return null;
3016
+ }
3017
+ if (/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----|gh[pousr]_[A-Za-z0-9]{20,}|(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?):\/\//i.test(normalized)) {
3018
+ return null;
3019
+ }
3020
+ return normalized;
3021
+ }
3022
+ function stringField(record, names) {
3023
+ for (const name of names) {
3024
+ const value = safeText(record[name]);
3025
+ if (value !== null)
3026
+ return value;
3027
+ }
3028
+ return null;
3029
+ }
3030
+ function severityOf(record) {
3031
+ const value = record.severity ?? record.level ?? record.priority ?? "unknown";
3032
+ return typeof value === "string" ? value.toLowerCase() : "unknown";
3033
+ }
3034
+ function highSeverity(severity) {
3035
+ return /^(?:red|critical|high|error|blocker|p0|p1)$/.test(severity);
3036
+ }
3037
+ function evidenceKind(path) {
3038
+ const name = basename3(path).toLowerCase();
3039
+ if (name.startsWith("review-"))
3040
+ return "review";
3041
+ if (name.startsWith("test-"))
3042
+ return "test";
3043
+ return "validation";
3044
+ }
3045
+ function observationFrom(record, path, archive) {
3046
+ const severity = severityOf(record);
3047
+ const suggestion = stringField(record, [
3048
+ "proposed_rule",
3049
+ "proposedRule",
3050
+ "suggestion",
3051
+ "recommendation",
3052
+ "remediation"
3053
+ ]);
3054
+ const issue = stringField(record, ["issue", "message", "error", "failure"]);
3055
+ const title = stringField(record, ["title", "name", "id", "code"]) ?? issue;
3056
+ let proposedRule = suggestion;
3057
+ if (proposedRule === null && issue !== null && highSeverity(severity)) {
3058
+ proposedRule = `\u5FC5\u987B\u589E\u52A0\u53EF\u91CD\u590D\u9A8C\u8BC1\uFF0C\u9632\u6B62\u4EE5\u4E0B\u95EE\u9898\u518D\u6B21\u51FA\u73B0\uFF1A${issue}`;
3059
+ }
3060
+ if (proposedRule === null || title === null)
3061
+ return null;
3062
+ const recordId = record.id ?? record.code ?? record.name ?? null;
3063
+ return {
3064
+ title,
3065
+ proposedRule,
3066
+ severity,
3067
+ evidence: {
3068
+ archive,
3069
+ path,
3070
+ kind: evidenceKind(path),
3071
+ record_id: typeof recordId === "string" ? recordId.slice(0, 120) : null
3072
+ }
3073
+ };
3074
+ }
3075
+ function collectObservations(value, path, archive, output, rejected) {
3076
+ if (Array.isArray(value)) {
3077
+ for (const item2 of value)
3078
+ collectObservations(item2, path, archive, output, rejected);
3079
+ return;
3080
+ }
3081
+ if (value === null || typeof value !== "object")
3082
+ return;
3083
+ const record = value;
3084
+ const candidate = observationFrom(record, path, archive);
3085
+ if (candidate !== null)
3086
+ output.push(candidate);
3087
+ else if (Object.keys(record).some((key) => ["suggestion", "recommendation", "proposed_rule", "proposedRule"].includes(key)))
3088
+ rejected.count += 1;
3089
+ for (const nested of Object.values(record)) {
3090
+ if (nested !== null && typeof nested === "object") {
3091
+ collectObservations(nested, path, archive, output, rejected);
3092
+ }
3093
+ }
3094
+ }
3095
+ async function evidenceFiles(root) {
3096
+ const archiveRoot = join7(root, ...ARCHIVE_ROOT.split("/"));
3097
+ const output = [];
3098
+ async function walk(directory) {
3099
+ let entries;
3100
+ try {
3101
+ entries = await readdir4(directory, { withFileTypes: true });
3102
+ } catch (error) {
3103
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
3104
+ return;
3105
+ throw error;
3106
+ }
3107
+ for (const entry of entries) {
3108
+ const path = join7(directory, entry.name);
3109
+ if (entry.isDirectory())
3110
+ await walk(path);
3111
+ else if (entry.isFile() && EVIDENCE_NAMES.some((pattern) => pattern.test(entry.name))) {
3112
+ if ((await stat3(path)).size <= MAX_EVIDENCE_BYTES)
3113
+ output.push(path);
3114
+ }
3115
+ }
3116
+ }
3117
+ await walk(archiveRoot);
3118
+ return output.sort();
3119
+ }
3120
+ function candidateKey(rule) {
3121
+ return normalizeText(rule).toLocaleLowerCase();
3122
+ }
3123
+ function buildCandidates(observations) {
3124
+ const grouped = /* @__PURE__ */ new Map();
3125
+ for (const observation of observations) {
3126
+ const key = candidateKey(observation.proposedRule);
3127
+ const values = grouped.get(key) ?? [];
3128
+ values.push(observation);
3129
+ grouped.set(key, values);
3130
+ }
3131
+ const candidates = [];
3132
+ for (const [key, values] of grouped) {
3133
+ const archives = new Set(values.map((value) => value.evidence.archive));
3134
+ const highest = values.find((value) => highSeverity(value.severity));
3135
+ if (archives.size < 2 && highest === void 0)
3136
+ continue;
3137
+ const representative = highest ?? values.at(0);
3138
+ if (representative === void 0)
3139
+ continue;
3140
+ const evidence = [...new Map(values.map((value) => [
3141
+ `${value.evidence.path}\0${value.evidence.record_id ?? ""}`,
3142
+ value.evidence
3143
+ ])).values()].sort((a, b) => a.path.localeCompare(b.path));
3144
+ candidates.push({
3145
+ id: `rule_${sha2562(key).slice(0, 16)}`,
3146
+ status: "candidate",
3147
+ title: representative.title,
3148
+ proposed_rule: representative.proposedRule,
3149
+ confidence: archives.size >= 2 && highest !== void 0 ? "high" : "medium",
3150
+ severity: representative.severity,
3151
+ occurrences: evidence.length,
3152
+ evidence
3153
+ });
3154
+ }
3155
+ return candidates.sort((a, b) => a.id.localeCompare(b.id));
3156
+ }
3157
+ async function atomicWrite2(path, content) {
3158
+ await mkdir5(dirname4(path), { recursive: true });
3159
+ const temporary = `${path}.${process.pid}.tmp`;
3160
+ await writeFile5(temporary, content, "utf8");
3161
+ await rename4(temporary, path);
3162
+ }
3163
+ async function synchronizeRuleCandidates(projectRoot, options = {}) {
3164
+ const root = resolve6(projectRoot);
3165
+ const files = await evidenceFiles(root);
3166
+ const sourceHashes = {};
3167
+ const observations = [];
3168
+ const rejected = { count: 0 };
3169
+ for (const path of files) {
3170
+ const relativePath = portable2(relative(root, path));
3171
+ const content2 = await readFile6(path, "utf8");
3172
+ sourceHashes[relativePath] = sha2562(content2);
3173
+ let parsed;
3174
+ try {
3175
+ parsed = JSON.parse(content2);
3176
+ } catch {
3177
+ continue;
3178
+ }
3179
+ const archive = relativePath.split("/")[2] ?? "unknown";
3180
+ collectObservations(parsed, relativePath, archive, observations, rejected);
3181
+ }
3182
+ const manifest = {
3183
+ schema_version: 1,
3184
+ source_hashes: sourceHashes,
3185
+ candidates: buildCandidates(observations)
3186
+ };
3187
+ const content = JSON.stringify(manifest, null, 2) + "\n";
3188
+ const destination = join7(root, ...CANDIDATE_PATH.split("/"));
3189
+ let current = null;
3190
+ try {
3191
+ current = await readFile6(destination, "utf8");
3192
+ } catch (error) {
3193
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
3194
+ throw error;
3195
+ }
3196
+ const changed = current !== content;
3197
+ if (changed && options.dryRun !== true)
3198
+ await atomicWrite2(destination, content);
3199
+ return {
3200
+ path: CANDIDATE_PATH,
3201
+ scanned: files.length,
3202
+ candidates: manifest.candidates.length,
3203
+ changed,
3204
+ rejected_untrusted: rejected.count
3205
+ };
2747
3206
  }
2748
3207
 
2749
3208
  // ../core/dist/project/refresh.js
2750
- import { createHash as createHash5 } from "node:crypto";
2751
- import { readFile as readFile5, readdir as readdir3, rmdir } from "node:fs/promises";
2752
- import { dirname as dirname3, join as join6, resolve as resolve5 } from "node:path";
3209
+ import { createHash as createHash7 } from "node:crypto";
3210
+ import { readFile as readFile7, readdir as readdir5, rmdir } from "node:fs/promises";
3211
+ import { dirname as dirname5, join as join8, resolve as resolve7 } from "node:path";
2753
3212
  import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
2754
3213
  var INSTALLED_STATE_PATH = ".harness/state/local/installed-harness-bundle.json";
2755
3214
  var CONTEXT_INDEX_PATH2 = ".harness/context-index.json";
2756
3215
  async function fileHex2(path) {
2757
3216
  try {
2758
- return createHash5("sha256").update(await readFile5(path)).digest("hex");
3217
+ return createHash7("sha256").update(await readFile7(path)).digest("hex");
2759
3218
  } catch (error) {
2760
3219
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
2761
3220
  return null;
@@ -2765,7 +3224,7 @@ async function fileHex2(path) {
2765
3224
  }
2766
3225
  async function readOptionalText(path) {
2767
3226
  try {
2768
- return await readFile5(path, "utf8");
3227
+ return await readFile7(path, "utf8");
2769
3228
  } catch (error) {
2770
3229
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
2771
3230
  return "";
@@ -2774,7 +3233,7 @@ async function readOptionalText(path) {
2774
3233
  }
2775
3234
  }
2776
3235
  async function readInstalledState(root) {
2777
- const content = await readOptionalText(join6(root, INSTALLED_STATE_PATH));
3236
+ const content = await readOptionalText(join8(root, INSTALLED_STATE_PATH));
2778
3237
  if (content === "") {
2779
3238
  return {
2780
3239
  profile: null,
@@ -2844,7 +3303,7 @@ async function readInstalledState(root) {
2844
3303
  };
2845
3304
  }
2846
3305
  async function readInstalledAgentConfiguration(projectRoot) {
2847
- const installed = await readInstalledState(resolve5(projectRoot));
3306
+ const installed = await readInstalledState(resolve7(projectRoot));
2848
3307
  return {
2849
3308
  agents: installed.adapters,
2850
3309
  profiles: Object.fromEntries(installed.adapters.map((agent) => [
@@ -2854,7 +3313,7 @@ async function readInstalledAgentConfiguration(projectRoot) {
2854
3313
  };
2855
3314
  }
2856
3315
  async function readContextIndexBundleHash(root) {
2857
- const content = await readOptionalText(join6(root, CONTEXT_INDEX_PATH2));
3316
+ const content = await readOptionalText(join8(root, CONTEXT_INDEX_PATH2));
2858
3317
  if (content === "")
2859
3318
  return null;
2860
3319
  try {
@@ -2866,13 +3325,13 @@ async function readContextIndexBundleHash(root) {
2866
3325
  }
2867
3326
  }
2868
3327
  async function pruneEmptyParentDirs(root, deletedPaths, boundaryPaths) {
2869
- const boundaries = new Set(boundaryPaths.map((path) => join6(root, path)));
3328
+ const boundaries = new Set(boundaryPaths.map((path) => join8(root, path)));
2870
3329
  for (const deleted of deletedPaths) {
2871
- let dir = dirname3(join6(root, deleted));
3330
+ let dir = dirname5(join8(root, deleted));
2872
3331
  while (dir.startsWith(root) && !boundaries.has(dir)) {
2873
3332
  let entries;
2874
3333
  try {
2875
- entries = await readdir3(dir);
3334
+ entries = await readdir5(dir);
2876
3335
  } catch {
2877
3336
  break;
2878
3337
  }
@@ -2883,7 +3342,7 @@ async function pruneEmptyParentDirs(root, deletedPaths, boundaryPaths) {
2883
3342
  } catch {
2884
3343
  break;
2885
3344
  }
2886
- dir = dirname3(dir);
3345
+ dir = dirname5(dir);
2887
3346
  }
2888
3347
  }
2889
3348
  }
@@ -2910,7 +3369,7 @@ function sortByTarget(items) {
2910
3369
  return [...items].sort((left, right) => left.target_path.localeCompare(right.target_path));
2911
3370
  }
2912
3371
  async function reconcileContextIndex(root, agents, profiles, manifests, codebuddySurface2, verifications) {
2913
- const existing = await readOptionalText(join6(root, CONTEXT_INDEX_PATH2));
3372
+ const existing = await readOptionalText(join8(root, CONTEXT_INDEX_PATH2));
2914
3373
  let codebase = {
2915
3374
  map: ".harness/codebase/map",
2916
3375
  status: "missing"
@@ -2963,7 +3422,7 @@ async function reconcileContextIndex(root, agents, profiles, manifests, codebudd
2963
3422
  }
2964
3423
  async function projectTransitionOperation(root, agents, profiles, codebuddySurface2) {
2965
3424
  const path = ".harness/project.yaml";
2966
- const content = await readOptionalText(join6(root, path));
3425
+ const content = await readOptionalText(join8(root, path));
2967
3426
  if (content === "")
2968
3427
  return null;
2969
3428
  const project = parseYaml3(content);
@@ -2998,11 +3457,11 @@ function mergeTargets(targets) {
2998
3457
  }).sort((left, right) => left.target_path.localeCompare(right.target_path));
2999
3458
  }
3000
3459
  async function reconcileMarkdownBlock(root, fileName, blockId, content, remove, ops, conflicts, preserved) {
3001
- const original = await readOptionalText(join6(root, fileName));
3460
+ const original = await readOptionalText(join8(root, fileName));
3002
3461
  const synthetic = {
3003
3462
  source_path: fileName,
3004
3463
  target_path: fileName,
3005
- sha256: createHash5("sha256").update(content).digest("hex"),
3464
+ sha256: createHash7("sha256").update(content).digest("hex"),
3006
3465
  bytes: new TextEncoder().encode(content)
3007
3466
  };
3008
3467
  let next;
@@ -3017,7 +3476,7 @@ async function reconcileMarkdownBlock(root, fileName, blockId, content, remove,
3017
3476
  next = refreshed.content;
3018
3477
  }
3019
3478
  } catch {
3020
- const current = original === "" ? null : createHash5("sha256").update(original).digest("hex");
3479
+ const current = original === "" ? null : createHash7("sha256").update(original).digest("hex");
3021
3480
  preserved.push(item(synthetic, "preserve", "MALFORMED_MANAGED_BLOCK", current, synthetic.sha256));
3022
3481
  conflicts.push(conflict(synthetic, "MALFORMED_MANAGED_BLOCK", current, synthetic.sha256));
3023
3482
  return;
@@ -3037,7 +3496,7 @@ function stateWithoutInstalledAt(value) {
3037
3496
  return copy;
3038
3497
  }
3039
3498
  async function refreshProject(options) {
3040
- const root = resolve5(options.projectRoot);
3499
+ const root = resolve7(options.projectRoot);
3041
3500
  const installed = await readInstalledState(root);
3042
3501
  const oldAgents = installed.adapters.length > 0 ? installed.adapters : ["claude-code"];
3043
3502
  const selectedAgents = sortHarnessAgents(options.agents);
@@ -3121,7 +3580,7 @@ async function refreshProject(options) {
3121
3580
  const newStateFiles = installed.files.filter((entry) => entry.owner === "shared" || !selectedSet.has(entry.owner));
3122
3581
  for (const target of newManaged) {
3123
3582
  const incoming = target.sha256;
3124
- const current = await fileHex2(join6(root, target.target_path));
3583
+ const current = await fileHex2(join8(root, target.target_path));
3125
3584
  if (current === null) {
3126
3585
  applied.push(item(target, "add", "MISSING_TARGET", null, incoming));
3127
3586
  ops.push({ operation: "add", path: target.target_path, content: target.bytes });
@@ -3149,7 +3608,7 @@ async function refreshProject(options) {
3149
3608
  }
3150
3609
  }
3151
3610
  for (const target of oldOnly) {
3152
- const current = await fileHex2(join6(root, target.target_path));
3611
+ const current = await fileHex2(join8(root, target.target_path));
3153
3612
  if (current === null) {
3154
3613
  continue;
3155
3614
  }
@@ -3192,7 +3651,7 @@ async function refreshProject(options) {
3192
3651
  const verifyEntries = [];
3193
3652
  for (const target of verifyTargets) {
3194
3653
  const rel = target.target_path.replace(/\\/g, "/");
3195
- const actual = await fileHex2(join6(root, target.target_path));
3654
+ const actual = await fileHex2(join8(root, target.target_path));
3196
3655
  verifyEntries.push({ relpath: rel, sha256: actual ?? "" });
3197
3656
  if (actual === null) {
3198
3657
  verifyMismatches.push({ relpath: rel, expected: target.sha256, actual: "<missing>" });
@@ -3225,19 +3684,19 @@ async function refreshProject(options) {
3225
3684
  owner: "shared",
3226
3685
  target_path: "AGENTS.md",
3227
3686
  block_id: AGENTS_CORE_BLOCK_ID,
3228
- content_sha256: createHash5("sha256").update(AGENTS_MANAGED_BLOCK_CONTENT).digest("hex")
3687
+ content_sha256: createHash7("sha256").update(AGENTS_MANAGED_BLOCK_CONTENT).digest("hex")
3229
3688
  },
3230
3689
  ...selectedSet.has("claude-code") ? [{
3231
3690
  owner: "claude-code",
3232
3691
  target_path: "CLAUDE.md",
3233
3692
  block_id: CLAUDE_BLOCK_ID,
3234
- content_sha256: createHash5("sha256").update(CLAUDE_MANAGED_BLOCK_CONTENT).digest("hex")
3693
+ content_sha256: createHash7("sha256").update(CLAUDE_MANAGED_BLOCK_CONTENT).digest("hex")
3235
3694
  }] : [],
3236
3695
  ...selectedSet.has("codebuddy") ? [{
3237
3696
  owner: "codebuddy",
3238
3697
  target_path: "CODEBUDDY.md",
3239
3698
  block_id: CODEBUDDY_BLOCK_ID,
3240
- content_sha256: createHash5("sha256").update(CODEBUDDY_MANAGED_BLOCK_CONTENT).digest("hex")
3699
+ content_sha256: createHash7("sha256").update(CODEBUDDY_MANAGED_BLOCK_CONTENT).digest("hex")
3241
3700
  }] : []
3242
3701
  ].sort((left, right) => left.target_path.localeCompare(right.target_path) || left.block_id.localeCompare(right.block_id));
3243
3702
  const filesByTarget = new Map(newStateFiles.map((entry) => [entry.target_path, entry]));
@@ -3253,7 +3712,7 @@ async function refreshProject(options) {
3253
3712
  files: [...filesByTarget.values()].sort((left, right) => left.target_path.localeCompare(right.target_path) || left.source_path.localeCompare(right.source_path)),
3254
3713
  managed_blocks: managedBlocks
3255
3714
  };
3256
- const existingState = await readOptionalText(join6(root, INSTALLED_STATE_PATH));
3715
+ const existingState = await readOptionalText(join8(root, INSTALLED_STATE_PATH));
3257
3716
  let existingParsed = null;
3258
3717
  try {
3259
3718
  existingParsed = existingState === "" ? null : JSON.parse(existingState);
@@ -3268,6 +3727,7 @@ async function refreshProject(options) {
3268
3727
  }
3269
3728
  if (!options.dryRun) {
3270
3729
  await runTransaction(root, ops, { kind: "refresh" });
3730
+ await synchronizeProjectRules(root, agents, codebuddySurface2);
3271
3731
  await pruneEmptyParentDirs(root, removed.map((item2) => item2.target_path), getAdapters(selectedAgents).flatMap((adapter) => adapter.pruneBoundaries({
3272
3732
  profile: profiles.get(adapter.name) ?? profile,
3273
3733
  codebuddySurface: codebuddySurface2
@@ -3307,7 +3767,7 @@ function buildMarkerCoreHash(text) {
3307
3767
  }
3308
3768
  }
3309
3769
  async function collectFreshness(options) {
3310
- const root = resolve5(options.projectRoot);
3770
+ const root = resolve7(options.projectRoot);
3311
3771
  const installed = await readInstalledState(root);
3312
3772
  const codebuddySurface2 = options.codebuddySurface ?? "both";
3313
3773
  const agents = [];
@@ -3352,18 +3812,18 @@ async function collectFreshness(options) {
3352
3812
  identity.adapterHash = sha256Bytes(canonicalJson(targets.map((target) => ({ path: target.target_path.replace(/\\/g, "/"), sha256: target.sha256 })).sort((a, b) => a.path.localeCompare(b.path))));
3353
3813
  const installedProjection = await Promise.all(targets.map(async (target) => ({
3354
3814
  path: target.target_path.replace(/\\/g, "/"),
3355
- sha256: await fileHex2(join6(root, target.target_path))
3815
+ sha256: await fileHex2(join8(root, target.target_path))
3356
3816
  })));
3357
3817
  identity.installedAdapterHash = sha256Bytes(canonicalJson(installedProjection.sort((a, b) => a.path.localeCompare(b.path))));
3358
3818
  const markerTarget = targets.find((target) => target.target_path.replace(/\\/g, "/").endsWith(`/${BUILD_MARKER_BUNDLE_PATH}`) || target.target_path === BUILD_MARKER_BUNDLE_PATH);
3359
3819
  if (markerTarget !== void 0) {
3360
- identity.installedCoreHash = buildMarkerCoreHash(await readOptionalText(join6(root, markerTarget.target_path)));
3820
+ identity.installedCoreHash = buildMarkerCoreHash(await readOptionalText(join8(root, markerTarget.target_path)));
3361
3821
  }
3362
3822
  const mismatchDetails = [];
3363
3823
  const contentEntries = [];
3364
3824
  for (const target of targets) {
3365
3825
  const rel = target.target_path.replace(/\\/g, "/");
3366
- const actual = await fileHex2(join6(root, target.target_path));
3826
+ const actual = await fileHex2(join8(root, target.target_path));
3367
3827
  contentEntries.push({ relpath: rel, sha256: actual ?? "" });
3368
3828
  if (actual === null) {
3369
3829
  mismatchDetails.push({ relpath: rel, expected: target.sha256, actual: "<missing>" });
@@ -3386,7 +3846,7 @@ async function collectFreshness(options) {
3386
3846
  const drifted = [];
3387
3847
  const missing = [];
3388
3848
  for (const target of targets) {
3389
- const current = await fileHex2(join6(root, target.target_path));
3849
+ const current = await fileHex2(join8(root, target.target_path));
3390
3850
  if (current === null) {
3391
3851
  missing.push(target.target_path);
3392
3852
  } else if (current !== target.sha256) {
@@ -3634,11 +4094,11 @@ function rawFindings(content) {
3634
4094
  if (rule.id === "HH_PASSWORD_VALUE" && isObviousPlaceholder(value)) {
3635
4095
  continue;
3636
4096
  }
3637
- const relative3 = match[0].indexOf(value);
4097
+ const relative4 = match[0].indexOf(value);
3638
4098
  findings.push({
3639
4099
  ruleId: rule.id,
3640
4100
  severity: rule.severity,
3641
- offset: match.index + Math.max(0, relative3),
4101
+ offset: match.index + Math.max(0, relative4),
3642
4102
  value
3643
4103
  });
3644
4104
  }
@@ -3750,8 +4210,8 @@ function generateProposalPreview(input, scanOptions = {}) {
3750
4210
  }
3751
4211
 
3752
4212
  // ../core/dist/push/credentials.js
3753
- import { readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
3754
- import { join as join7 } from "node:path";
4213
+ import { readFile as readFile8, writeFile as writeFile6 } from "node:fs/promises";
4214
+ import { join as join9 } from "node:path";
3755
4215
  import { parse as parseYaml4, stringify as stringifyYaml3 } from "yaml";
3756
4216
  var CREDENTIALS_LOCAL_RELATIVE = ".harness/credentials.local.yaml";
3757
4217
  var CREDENTIALS_GITIGNORE_LINES = [
@@ -3821,7 +4281,7 @@ function mergeLocalCredentials(existing, patch) {
3821
4281
  }
3822
4282
  async function readLocalCredentials(projectRoot) {
3823
4283
  try {
3824
- const raw = await readFile6(join7(projectRoot, CREDENTIALS_LOCAL_RELATIVE), "utf8");
4284
+ const raw = await readFile8(join9(projectRoot, CREDENTIALS_LOCAL_RELATIVE), "utf8");
3825
4285
  return parseLocalCredentials(parseYaml4(raw));
3826
4286
  } catch (error) {
3827
4287
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -3832,13 +4292,13 @@ async function readLocalCredentials(projectRoot) {
3832
4292
  }
3833
4293
  async function writeLocalCredentials(projectRoot, credentials) {
3834
4294
  const normalized = validateLocalCredentials(credentials);
3835
- await writeFile4(join7(projectRoot, CREDENTIALS_LOCAL_RELATIVE), stringifyYaml3(normalized, { sortMapEntries: true }) + "\n", "utf8");
4295
+ await writeFile6(join9(projectRoot, CREDENTIALS_LOCAL_RELATIVE), stringifyYaml3(normalized, { sortMapEntries: true }) + "\n", "utf8");
3836
4296
  }
3837
4297
  async function ensureCredentialsGitignore(projectRoot) {
3838
- const gitignorePath = join7(projectRoot, ".gitignore");
4298
+ const gitignorePath = join9(projectRoot, ".gitignore");
3839
4299
  let content = "";
3840
4300
  try {
3841
- content = await readFile6(gitignorePath, "utf8");
4301
+ content = await readFile8(gitignorePath, "utf8");
3842
4302
  } catch (error) {
3843
4303
  if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
3844
4304
  throw error;
@@ -3861,7 +4321,7 @@ async function ensureCredentialsGitignore(projectRoot) {
3861
4321
  return;
3862
4322
  }
3863
4323
  const suffix = content.length === 0 || content.endsWith("\n") ? "" : "\n";
3864
- await writeFile4(gitignorePath, content + suffix + missing.join("\n") + "\n", "utf8");
4324
+ await writeFile6(gitignorePath, content + suffix + missing.join("\n") + "\n", "utf8");
3865
4325
  }
3866
4326
  function resolvePushAuth(input) {
3867
4327
  const tokenEnv = input.tokenEnv ?? input.projectTokenEnv;
@@ -3889,28 +4349,28 @@ function resolvePushAuth(input) {
3889
4349
  }
3890
4350
 
3891
4351
  // ../core/dist/push/push.js
3892
- import { lstat as lstat3, readFile as readFile10, readdir as readdir4, rm as rm5 } from "node:fs/promises";
3893
- import { join as join11, relative, resolve as resolve7 } from "node:path";
4352
+ import { lstat as lstat3, readFile as readFile12, readdir as readdir6, rm as rm5 } from "node:fs/promises";
4353
+ import { join as join13, relative as relative2, resolve as resolve9 } from "node:path";
3894
4354
  import { parse as parseYaml5, stringify as stringifyYaml4 } from "yaml";
3895
4355
 
3896
4356
  // ../core/dist/state/baseline.js
3897
- import { readFile as readFile7 } from "node:fs/promises";
3898
- import { join as join8 } from "node:path";
4357
+ import { readFile as readFile9 } from "node:fs/promises";
4358
+ import { join as join10 } from "node:path";
3899
4359
  async function readBaseline(projectRoot) {
3900
- const content = await readFile7(join8(stateLayout(projectRoot).baseline, "manifest.json"), "utf8");
4360
+ const content = await readFile9(join10(stateLayout(projectRoot).baseline, "manifest.json"), "utf8");
3901
4361
  return baselineManifestSchema.parse(JSON.parse(content));
3902
4362
  }
3903
4363
 
3904
4364
  // ../core/dist/state/locks.js
3905
4365
  import { randomUUID as randomUUID3 } from "node:crypto";
3906
- import { readFile as readFile8, rename as rename3, rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
3907
- import { join as join9 } from "node:path";
4366
+ import { readFile as readFile10, rename as rename5, rm as rm3, writeFile as writeFile7 } from "node:fs/promises";
4367
+ import { join as join11 } from "node:path";
3908
4368
  async function readLock(path) {
3909
- return JSON.parse(await readFile8(path, "utf8"));
4369
+ return JSON.parse(await readFile10(path, "utf8"));
3910
4370
  }
3911
4371
  async function acquireProtocolLock(projectRoot, operation, options = {}) {
3912
4372
  const layout = await ensureStateLayout(projectRoot);
3913
- const lockPath = join9(layout.locks, "protocol.lock");
4373
+ const lockPath = join11(layout.locks, "protocol.lock");
3914
4374
  const now = options.now ?? Date.now();
3915
4375
  const staleAfterMs = options.staleAfterMs ?? 15 * 60 * 1e3;
3916
4376
  const record = {
@@ -3923,7 +4383,7 @@ async function acquireProtocolLock(projectRoot, operation, options = {}) {
3923
4383
  };
3924
4384
  for (let attempt = 0; attempt < 2; attempt += 1) {
3925
4385
  try {
3926
- await writeFile5(lockPath, JSON.stringify(record, null, 2), { flag: "wx" });
4386
+ await writeFile7(lockPath, JSON.stringify(record, null, 2), { flag: "wx" });
3927
4387
  return {
3928
4388
  path: lockPath,
3929
4389
  operation,
@@ -3945,15 +4405,15 @@ async function acquireProtocolLock(projectRoot, operation, options = {}) {
3945
4405
  if (now - current.heartbeat_at_ms <= staleAfterMs) {
3946
4406
  throw new Error("protocol lock is active for operation " + current.operation, { cause: error });
3947
4407
  }
3948
- await rename3(lockPath, lockPath + ".stale-" + randomUUID3());
4408
+ await rename5(lockPath, lockPath + ".stale-" + randomUUID3());
3949
4409
  }
3950
4410
  }
3951
4411
  throw new Error("unable to acquire protocol lock");
3952
4412
  }
3953
4413
 
3954
4414
  // ../core/dist/sync/synchronize.js
3955
- import { lstat as lstat2, readFile as readFile9, rm as rm4 } from "node:fs/promises";
3956
- import { join as join10, resolve as resolve6 } from "node:path";
4415
+ import { lstat as lstat2, readFile as readFile11, rm as rm4 } from "node:fs/promises";
4416
+ import { join as join12, resolve as resolve8 } from "node:path";
3957
4417
 
3958
4418
  // ../core/dist/update/conflicts.js
3959
4419
  function operationTargetPath(operation) {
@@ -4235,8 +4695,8 @@ async function pathExists(path) {
4235
4695
  }
4236
4696
  }
4237
4697
  async function optionalContent(root, path) {
4238
- const full = join10(root, path);
4239
- return await pathExists(full) ? readFile9(full, "utf8") : null;
4698
+ const full = join12(root, path);
4699
+ return await pathExists(full) ? readFile11(full, "utf8") : null;
4240
4700
  }
4241
4701
  function manifestPayloadHash(manifest) {
4242
4702
  const payload = { ...manifest };
@@ -4248,10 +4708,10 @@ async function loadBlob(root, client, artifactId, operation, requestId, dryRun)
4248
4708
  return null;
4249
4709
  }
4250
4710
  const hash = operation.content_sha256;
4251
- const cacheRoot = join10(root, ".harness", "cache", "server-artifacts", artifactId);
4252
- const cachePath = join10(cacheRoot, "blobs", hash.replace(":", "_"));
4711
+ const cacheRoot = join12(root, ".harness", "cache", "server-artifacts", artifactId);
4712
+ const cachePath = join12(cacheRoot, "blobs", hash.replace(":", "_"));
4253
4713
  if (await pathExists(cachePath) && await sha256File(cachePath) === hash) {
4254
- return readFile9(cachePath, "utf8");
4714
+ return readFile11(cachePath, "utf8");
4255
4715
  }
4256
4716
  const bytes = await client.downloadArtifactBlob(artifactId, hash, requestId);
4257
4717
  if (bytes.byteLength !== operation.size_bytes || sha256Bytes(bytes) !== hash) {
@@ -4356,7 +4816,7 @@ async function planSingleArtifact(root, baseline, manifest, client, requestId, d
4356
4816
  });
4357
4817
  }
4358
4818
  async function saveConflictReport(root, requestId, manifest, plan) {
4359
- const reportPath = join10(root, ".harness", "reports", "conflicts-" + requestId + ".json");
4819
+ const reportPath = join12(root, ".harness", "reports", "conflicts-" + requestId + ".json");
4360
4820
  await atomicWriteJson(reportPath, {
4361
4821
  schema_version: 1,
4362
4822
  request_id: requestId,
@@ -4367,7 +4827,7 @@ async function saveConflictReport(root, requestId, manifest, plan) {
4367
4827
  });
4368
4828
  }
4369
4829
  async function synchronizeArtifacts(options, initialBaseline) {
4370
- const root = resolve6(options.projectRoot);
4830
+ const root = resolve8(options.projectRoot);
4371
4831
  const projectId = options.project.project.project_id;
4372
4832
  if (projectId === null) {
4373
4833
  throw new Error("PROJECT_NOT_BOUND");
@@ -4474,7 +4934,7 @@ async function synchronizeArtifacts(options, initialBaseline) {
4474
4934
  }
4475
4935
  continue;
4476
4936
  }
4477
- await atomicWriteJson(join10(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
4937
+ await atomicWriteJson(join12(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
4478
4938
  const lock = await acquireProtocolLock(root, "update", { requestId: options.requestId });
4479
4939
  try {
4480
4940
  const nextBaseline = applyBaselineUpdates(baseline, plan);
@@ -4544,7 +5004,7 @@ function baselineEntryFromOperation(manifest, operation, localContent) {
4544
5004
  };
4545
5005
  }
4546
5006
  async function advanceBaselineFromArtifact(options, baseline) {
4547
- const root = resolve6(options.projectRoot);
5007
+ const root = resolve8(options.projectRoot);
4548
5008
  if (options.manifest.project_version === null) {
4549
5009
  throw new Error("artifact manifest missing project_version");
4550
5010
  }
@@ -4621,7 +5081,8 @@ var PushWorkflowError = class extends Error {
4621
5081
  };
4622
5082
  var SHARED_MANAGED_ROOTS = [
4623
5083
  ".harness/knowledge",
4624
- ".harness/codebase"
5084
+ ".harness/codebase",
5085
+ ".harness/rules"
4625
5086
  ];
4626
5087
  var SHARED_MANAGED_FILES = [
4627
5088
  "AGENTS.md",
@@ -4644,29 +5105,29 @@ async function walkFiles(root, current, output) {
4644
5105
  if (!await exists3(current)) {
4645
5106
  return;
4646
5107
  }
4647
- for (const item2 of await readdir4(current, { withFileTypes: true })) {
4648
- const path = join11(current, item2.name);
5108
+ for (const item2 of await readdir6(current, { withFileTypes: true })) {
5109
+ const path = join13(current, item2.name);
4649
5110
  if (item2.isSymbolicLink()) {
4650
5111
  throw new PushWorkflowError("symlink is not pushable", 6, "UNSAFE_SYMLINK");
4651
5112
  }
4652
5113
  if (item2.isDirectory()) {
4653
5114
  await walkFiles(root, path, output);
4654
5115
  } else if (item2.isFile()) {
4655
- output.push(normalizeManagedPath(relative(root, path).replaceAll("\\", "/")));
5116
+ output.push(normalizeManagedPath(relative2(root, path).replaceAll("\\", "/")));
4656
5117
  }
4657
5118
  }
4658
5119
  }
4659
5120
  async function walkArchiveSummaries(root, output) {
4660
- const archiveRoot = join11(root, ".harness", "archive");
5121
+ const archiveRoot = join13(root, ".harness", "archive");
4661
5122
  if (!await exists3(archiveRoot))
4662
5123
  return;
4663
- for (const item2 of await readdir4(archiveRoot, { withFileTypes: true })) {
5124
+ for (const item2 of await readdir6(archiveRoot, { withFileTypes: true })) {
4664
5125
  if (item2.isSymbolicLink()) {
4665
5126
  throw new PushWorkflowError("symlink is not pushable", 6, "UNSAFE_SYMLINK");
4666
5127
  }
4667
5128
  if (!item2.isDirectory())
4668
5129
  continue;
4669
- const summaryPath = join11(archiveRoot, item2.name, "reports", "final", "summary-data.json");
5130
+ const summaryPath = join13(archiveRoot, item2.name, "reports", "final", "summary-data.json");
4670
5131
  try {
4671
5132
  const stats = await lstat3(summaryPath);
4672
5133
  if (stats.isSymbolicLink()) {
@@ -4679,7 +5140,7 @@ async function walkArchiveSummaries(root, output) {
4679
5140
  continue;
4680
5141
  throw error;
4681
5142
  }
4682
- const relativePath = normalizeManagedPath(relative(root, summaryPath).replaceAll("\\", "/"));
5143
+ const relativePath = normalizeManagedPath(relative2(root, summaryPath).replaceAll("\\", "/"));
4683
5144
  if (ARCHIVE_SUMMARY_PATH.test(relativePath)) {
4684
5145
  output.push(relativePath);
4685
5146
  }
@@ -4694,19 +5155,19 @@ function enabledHarnessAgents(project) {
4694
5155
  async function walkHarnessEntries(root, directory, output) {
4695
5156
  if (!await exists3(directory))
4696
5157
  return;
4697
- for (const item2 of await readdir4(directory, { withFileTypes: true })) {
5158
+ for (const item2 of await readdir6(directory, { withFileTypes: true })) {
4698
5159
  if (item2.name.startsWith("harness-")) {
4699
- const path = join11(directory, item2.name);
5160
+ const path = join13(directory, item2.name);
4700
5161
  if (item2.isDirectory()) {
4701
5162
  await walkFiles(root, path, output);
4702
5163
  } else if (item2.isFile()) {
4703
- output.push(normalizeManagedPath(relative(root, path).replaceAll("\\", "/")));
5164
+ output.push(normalizeManagedPath(relative2(root, path).replaceAll("\\", "/")));
4704
5165
  }
4705
5166
  }
4706
5167
  }
4707
5168
  }
4708
5169
  async function managedFiles(projectRoot, project) {
4709
- const root = resolve7(projectRoot);
5170
+ const root = resolve9(projectRoot);
4710
5171
  const paths = [];
4711
5172
  const adapters = getAdapters(enabledHarnessAgents(project));
4712
5173
  const managedFiles2 = [
@@ -4715,26 +5176,26 @@ async function managedFiles(projectRoot, project) {
4715
5176
  ...adapters.some((adapter) => adapter.name === "codebuddy") ? ["CODEBUDDY.md"] : []
4716
5177
  ];
4717
5178
  for (const path of managedFiles2) {
4718
- if (await exists3(join11(root, path))) {
5179
+ if (await exists3(join13(root, path))) {
4719
5180
  paths.push(path);
4720
5181
  }
4721
5182
  }
4722
5183
  for (const path of SHARED_MANAGED_ROOTS) {
4723
- await walkFiles(root, join11(root, path), paths);
5184
+ await walkFiles(root, join13(root, path), paths);
4724
5185
  }
4725
5186
  await walkArchiveSummaries(root, paths);
4726
5187
  for (const adapter of adapters) {
4727
5188
  if (adapter.rulesRoot !== null) {
4728
- await walkFiles(root, join11(root, adapter.rulesRoot), paths);
5189
+ await walkHarnessEntries(root, join13(root, adapter.rulesRoot), paths);
4729
5190
  }
4730
- await walkHarnessEntries(root, join11(root, adapter.skillsRoot), paths);
5191
+ await walkHarnessEntries(root, join13(root, adapter.skillsRoot), paths);
4731
5192
  if (adapter.agentsRoot !== null) {
4732
- await walkHarnessEntries(root, join11(root, adapter.agentsRoot), paths);
5193
+ await walkHarnessEntries(root, join13(root, adapter.agentsRoot), paths);
4733
5194
  }
4734
5195
  }
4735
5196
  const result = {};
4736
5197
  for (const path of [...new Set(paths)].sort()) {
4737
- result[path] = await readFile10(join11(root, path), "utf8");
5198
+ result[path] = await readFile12(join13(root, path), "utf8");
4738
5199
  }
4739
5200
  return result;
4740
5201
  }
@@ -4743,14 +5204,14 @@ function proposalBaseline(baseline) {
4743
5204
  }
4744
5205
  async function readProject(root) {
4745
5206
  try {
4746
- return projectConfigSchema.parse(parseYaml5(await readFile10(join11(root, ".harness", "project.yaml"), "utf8")));
5207
+ return projectConfigSchema.parse(parseYaml5(await readFile12(join13(root, ".harness", "project.yaml"), "utf8")));
4747
5208
  } catch {
4748
5209
  throw new PushWorkflowError("project configuration is missing or invalid", 3, "PROJECT_CONFIG_INVALID");
4749
5210
  }
4750
5211
  }
4751
5212
  async function readOptionalJson(path) {
4752
5213
  try {
4753
- return JSON.parse(await readFile10(path, "utf8"));
5214
+ return JSON.parse(await readFile12(path, "utf8"));
4754
5215
  } catch (error) {
4755
5216
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
4756
5217
  return null;
@@ -4762,7 +5223,7 @@ async function clientIdFor(root, explicit) {
4762
5223
  if (explicit !== void 0) {
4763
5224
  return explicit;
4764
5225
  }
4765
- const path = join11(root, ".harness", "state", "local", "client.json");
5226
+ const path = join13(root, ".harness", "state", "local", "client.json");
4766
5227
  const existing = await readOptionalJson(path);
4767
5228
  if (typeof existing?.client_id === "string" && /^cli_[A-Za-z0-9_-]+$/.test(existing.client_id)) {
4768
5229
  return existing.client_id;
@@ -4958,7 +5419,7 @@ async function bindProject(root, project, baseline, projectId) {
4958
5419
  return { project: nextProject, baseline: nextBaseline };
4959
5420
  }
4960
5421
  async function pushProject(options) {
4961
- const root = resolve7(options.projectRoot);
5422
+ const root = resolve9(options.projectRoot);
4962
5423
  let project = await readProject(root);
4963
5424
  let baseline = await readBaseline(root);
4964
5425
  const profile = parseHarnessProfile(project.project.profiles[0]);
@@ -5024,7 +5485,7 @@ async function pushProject(options) {
5024
5485
  if (options.confirmProposal !== void 0 && !await options.confirmProposal(preview)) {
5025
5486
  return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
5026
5487
  }
5027
- const workflowPath = join11(root, ".harness", "state", "local", "push-workflow.json");
5488
+ const workflowPath = join13(root, ".harness", "state", "local", "push-workflow.json");
5028
5489
  const priorWorkflow = await readOptionalJson(workflowPath);
5029
5490
  const provisionalRequestId = priorWorkflow?.local_project_key === project.project.local_project_key ? priorWorkflow.request_id : uuidV7();
5030
5491
  const lock = await acquireProtocolLock(root, "push", { requestId: provisionalRequestId });
@@ -5179,7 +5640,7 @@ async function pushProject(options) {
5179
5640
  pushWarning = "BASELINE_ADVANCE_DEFERRED";
5180
5641
  }
5181
5642
  }
5182
- await atomicWriteJson(join11(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
5643
+ await atomicWriteJson(join13(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
5183
5644
  schema_version: 1,
5184
5645
  request_id: requestId,
5185
5646
  project_id: projectId,
@@ -5224,14 +5685,14 @@ async function pushProject(options) {
5224
5685
  }
5225
5686
 
5226
5687
  // ../core/dist/state/cleanup.js
5227
- import { readFile as readFile11, readdir as readdir5, rm as rm6 } from "node:fs/promises";
5228
- import { join as join12 } from "node:path";
5688
+ import { readFile as readFile13, readdir as readdir7, rm as rm6 } from "node:fs/promises";
5689
+ import { join as join14 } from "node:path";
5229
5690
  function isSafeEntryName(name) {
5230
5691
  return name.length > 0 && !name.includes("/") && !name.includes("\\") && name !== "." && name !== ".." && !name.includes("\0");
5231
5692
  }
5232
5693
  async function listDir(path) {
5233
5694
  try {
5234
- return await readdir5(path);
5695
+ return await readdir7(path);
5235
5696
  } catch (error) {
5236
5697
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
5237
5698
  return [];
@@ -5249,7 +5710,7 @@ async function cleanupProject(options) {
5249
5710
  continue;
5250
5711
  let journal;
5251
5712
  try {
5252
- journal = JSON.parse(await readFile11(join12(layout.transactions, name, "journal.json"), "utf8"));
5713
+ journal = JSON.parse(await readFile13(join14(layout.transactions, name, "journal.json"), "utf8"));
5253
5714
  } catch {
5254
5715
  continue;
5255
5716
  }
@@ -5267,7 +5728,7 @@ async function cleanupProject(options) {
5267
5728
  continue;
5268
5729
  pruned.push(entry.id);
5269
5730
  if (!options.dryRun) {
5270
- await rm6(join12(layout.transactions, entry.id), { recursive: true, force: true });
5731
+ await rm6(join14(layout.transactions, entry.id), { recursive: true, force: true });
5271
5732
  }
5272
5733
  }
5273
5734
  }
@@ -5276,7 +5737,7 @@ async function cleanupProject(options) {
5276
5737
  continue;
5277
5738
  removedCache.push(name);
5278
5739
  if (!options.dryRun) {
5279
- await rm6(join12(layout.serverArtifacts, name), { recursive: true, force: true });
5740
+ await rm6(join14(layout.serverArtifacts, name), { recursive: true, force: true });
5280
5741
  }
5281
5742
  }
5282
5743
  return {
@@ -5336,10 +5797,10 @@ var SKILL_TARGET_AGENTS = Object.freeze([
5336
5797
  ]);
5337
5798
 
5338
5799
  // ../core/dist/transaction/recovery.js
5339
- import { readFile as readFile12, readdir as readdir6, rm as rm7, stat as stat3 } from "node:fs/promises";
5340
- import { join as join13 } from "node:path";
5800
+ import { readFile as readFile14, readdir as readdir8, rm as rm7, stat as stat4 } from "node:fs/promises";
5801
+ import { join as join15 } from "node:path";
5341
5802
  async function recoverTransaction(projectRoot, transactionId) {
5342
- const journal = JSON.parse(await readFile12(join13(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
5803
+ const journal = JSON.parse(await readFile14(join15(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
5343
5804
  if (journal.state === "committed") {
5344
5805
  return { transactionId, status: "committed" };
5345
5806
  }
@@ -5356,7 +5817,7 @@ async function listTransactions(projectRoot) {
5356
5817
  const root = stateLayout(projectRoot).transactions;
5357
5818
  let names;
5358
5819
  try {
5359
- names = await readdir6(root);
5820
+ names = await readdir8(root);
5360
5821
  } catch (error) {
5361
5822
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
5362
5823
  return [];
@@ -5366,7 +5827,7 @@ async function listTransactions(projectRoot) {
5366
5827
  const transactions = [];
5367
5828
  for (const transactionId of names) {
5368
5829
  try {
5369
- const journal = JSON.parse(await readFile12(join13(root, transactionId, "journal.json"), "utf8"));
5830
+ const journal = JSON.parse(await readFile14(join15(root, transactionId, "journal.json"), "utf8"));
5370
5831
  transactions.push({
5371
5832
  transactionId,
5372
5833
  kind: journal.kind,
@@ -5383,7 +5844,7 @@ async function pendingTransactions(projectRoot) {
5383
5844
  }
5384
5845
  async function pathExists2(path) {
5385
5846
  try {
5386
- await stat3(path);
5847
+ await stat4(path);
5387
5848
  return true;
5388
5849
  } catch (error) {
5389
5850
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -5397,11 +5858,11 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
5397
5858
  if (latest === void 0) {
5398
5859
  throw new Error("no committed update transaction is available for rollback");
5399
5860
  }
5400
- const transactionRoot = join13(stateLayout(projectRoot).transactions, latest.transactionId);
5401
- const journal = JSON.parse(await readFile12(join13(transactionRoot, "journal.json"), "utf8"));
5402
- const after = JSON.parse(await readFile12(join13(transactionRoot, "after", "manifest.json"), "utf8"));
5861
+ const transactionRoot = join15(stateLayout(projectRoot).transactions, latest.transactionId);
5862
+ const journal = JSON.parse(await readFile14(join15(transactionRoot, "journal.json"), "utf8"));
5863
+ const after = JSON.parse(await readFile14(join15(transactionRoot, "after", "manifest.json"), "utf8"));
5403
5864
  for (const entry of after) {
5404
- const target = join13(projectRoot, entry.path);
5865
+ const target = join15(projectRoot, entry.path);
5405
5866
  const exists6 = await pathExists2(target);
5406
5867
  if (exists6 !== entry.exists || exists6 && await sha256File(target) !== entry.hash) {
5407
5868
  throw new Error("cannot rollback dirty path: " + entry.path);
@@ -5414,10 +5875,10 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
5414
5875
  continue;
5415
5876
  }
5416
5877
  seen.add(snapshot.path);
5417
- const target = join13(projectRoot, snapshot.path);
5878
+ const target = join15(projectRoot, snapshot.path);
5418
5879
  const exists6 = await pathExists2(target);
5419
5880
  if (snapshot.existed && snapshot.snapshot_name !== null) {
5420
- const content = await readFile12(join13(transactionRoot, "before", snapshot.snapshot_name));
5881
+ const content = await readFile14(join15(transactionRoot, "before", snapshot.snapshot_name));
5421
5882
  operations.push({
5422
5883
  operation: exists6 ? "modify" : "add",
5423
5884
  path: snapshot.path,
@@ -5446,7 +5907,7 @@ async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Dat
5446
5907
  if (!removable) {
5447
5908
  continue;
5448
5909
  }
5449
- await rm7(join13(stateLayout(projectRoot).transactions, item2.transactionId), {
5910
+ await rm7(join15(stateLayout(projectRoot).transactions, item2.transactionId), {
5450
5911
  recursive: true,
5451
5912
  force: true
5452
5913
  });
@@ -5456,8 +5917,8 @@ async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Dat
5456
5917
  }
5457
5918
 
5458
5919
  // ../core/dist/update/update.js
5459
- import { readFile as readFile13 } from "node:fs/promises";
5460
- import { join as join14, resolve as resolve8 } from "node:path";
5920
+ import { readFile as readFile15 } from "node:fs/promises";
5921
+ import { join as join16, resolve as resolve10 } from "node:path";
5461
5922
  import { parse as parseYaml8 } from "yaml";
5462
5923
  var UpdateWorkflowError = class extends Error {
5463
5924
  exitCode;
@@ -5470,10 +5931,10 @@ var UpdateWorkflowError = class extends Error {
5470
5931
  }
5471
5932
  };
5472
5933
  async function updateProject(options) {
5473
- const root = resolve8(options.projectRoot);
5934
+ const root = resolve10(options.projectRoot);
5474
5935
  let project;
5475
5936
  try {
5476
- project = projectConfigSchema.parse(parseYaml8(await readFile13(join14(root, ".harness", "project.yaml"), "utf8")));
5937
+ project = projectConfigSchema.parse(parseYaml8(await readFile15(join16(root, ".harness", "project.yaml"), "utf8")));
5477
5938
  } catch {
5478
5939
  throw new UpdateWorkflowError("project configuration is invalid", 3, "PROJECT_CONFIG_INVALID");
5479
5940
  }
@@ -5523,6 +5984,13 @@ async function updateProject(options) {
5523
5984
  ...options.confirmConflictStrategy === void 0 ? {} : { confirmConflictStrategy: options.confirmConflictStrategy },
5524
5985
  ...options.transactionOptions === void 0 ? {} : { transactionOptions: options.transactionOptions }
5525
5986
  }, baseline);
5987
+ if (!options.dryRun) {
5988
+ const agents = project.adapters.enabled.flatMap((agent) => {
5989
+ const parsed = harnessAgentSchema.safeParse(agent);
5990
+ return parsed.success ? [parsed.data] : [];
5991
+ });
5992
+ await synchronizeProjectRules(root, agents, project.adapter_options?.codebuddy?.surface ?? "both");
5993
+ }
5526
5994
  return result;
5527
5995
  } catch (error) {
5528
5996
  if (error instanceof UpdateWorkflowError) {
@@ -5548,8 +6016,8 @@ async function updateProject(options) {
5548
6016
  }
5549
6017
 
5550
6018
  // src/config/init-config.ts
5551
- import { isAbsolute as isAbsolute2, join as join15 } from "node:path";
5552
- import { readFile as readFile14 } from "node:fs/promises";
6019
+ import { isAbsolute as isAbsolute2, join as join17 } from "node:path";
6020
+ import { readFile as readFile16 } from "node:fs/promises";
5553
6021
  var InitConfigurationError = class extends Error {
5554
6022
  exitCode;
5555
6023
  code;
@@ -5632,9 +6100,9 @@ function hasOwn(record, key) {
5632
6100
  async function resolveInitConfig(cwd, flags, prompts = {}, warnings = []) {
5633
6101
  let fileConfig = {};
5634
6102
  if (flags.config !== void 0) {
5635
- const path = isAbsolute2(flags.config) ? flags.config : join15(cwd, flags.config);
6103
+ const path = isAbsolute2(flags.config) ? flags.config : join17(cwd, flags.config);
5636
6104
  try {
5637
- fileConfig = JSON.parse(await readFile14(path, "utf8"));
6105
+ fileConfig = JSON.parse(await readFile16(path, "utf8"));
5638
6106
  } catch (error) {
5639
6107
  throw new InitConfigurationError(
5640
6108
  "unable to read init config: " + (error instanceof Error ? error.message : String(error)),
@@ -5716,8 +6184,8 @@ function serializeCliResult(result) {
5716
6184
  }
5717
6185
 
5718
6186
  // src/config/codebuddy-setup.ts
5719
- import { mkdir as mkdir4, readFile as readFile15, readdir as readdir7, stat as stat4, writeFile as writeFile6 } from "node:fs/promises";
5720
- import { basename as basename2, dirname as dirname4, extname, join as join16 } from "node:path";
6187
+ import { mkdir as mkdir6, readFile as readFile17, readdir as readdir9, stat as stat5, writeFile as writeFile8 } from "node:fs/promises";
6188
+ import { basename as basename4, dirname as dirname6, extname as extname2, join as join18 } from "node:path";
5721
6189
  var MANAGED_RULE_NAMES = /* @__PURE__ */ new Set([
5722
6190
  "harness-general.md",
5723
6191
  "harness-general.mdc",
@@ -5728,7 +6196,7 @@ var SENSITIVE_ASSIGNMENT = /(?:password|passwd|token|secret|access[_-]?key|priva
5728
6196
  var PRIVATE_KEY_BLOCK = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i;
5729
6197
  async function exists4(path) {
5730
6198
  try {
5731
- await stat4(path);
6199
+ await stat5(path);
5732
6200
  return true;
5733
6201
  } catch {
5734
6202
  return false;
@@ -5736,35 +6204,59 @@ async function exists4(path) {
5736
6204
  }
5737
6205
  async function readJsonObject(path) {
5738
6206
  try {
5739
- const parsed = JSON.parse(await readFile15(path, "utf8"));
6207
+ const parsed = JSON.parse(await readFile17(path, "utf8"));
5740
6208
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
5741
6209
  } catch (error) {
5742
6210
  if (error instanceof Error && "code" in error && error.code === "ENOENT") return {};
5743
6211
  return null;
5744
6212
  }
5745
6213
  }
5746
- async function inspectCodeBuddySetup(projectRoot) {
5747
- const rulesRoot = join16(projectRoot, ".claude", "rules");
5748
- let claudeRules = [];
6214
+ async function inspectCodeBuddySetup(projectRoot, surface = "both") {
6215
+ const rulesRoot = join18(projectRoot, ".claude", "rules");
6216
+ let ruleNames = [];
5749
6217
  try {
5750
- claudeRules = (await readdir7(rulesRoot, { withFileTypes: true })).filter((entry) => entry.isFile() && [".md", ".mdc"].includes(extname(entry.name).toLowerCase())).map((entry) => entry.name).filter((name) => !MANAGED_RULE_NAMES.has(name)).sort();
6218
+ 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();
5751
6219
  } catch (error) {
5752
6220
  if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
5753
6221
  }
5754
- const mcp = await readJsonObject(join16(projectRoot, ".mcp.json"));
6222
+ const claudeRules = [];
6223
+ const currentClaudeRules = [];
6224
+ const conflictingClaudeRules = [];
6225
+ for (const name of ruleNames) {
6226
+ const sourceContent = await readFile17(join18(rulesRoot, name), "utf8");
6227
+ const targetContents = await Promise.all(
6228
+ destinationTargets(projectRoot, surface, name).map(async (target) => {
6229
+ try {
6230
+ return await readFile17(target, "utf8");
6231
+ } catch (error) {
6232
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
6233
+ throw error;
6234
+ }
6235
+ })
6236
+ );
6237
+ if (targetContents.some((content) => content === null)) claudeRules.push(name);
6238
+ if (targetContents.some((content) => content !== null && content !== sourceContent)) {
6239
+ conflictingClaudeRules.push(name);
6240
+ } else if (targetContents.length > 0 && targetContents.every((content) => content === sourceContent)) {
6241
+ currentClaudeRules.push(name);
6242
+ }
6243
+ }
6244
+ const mcp = await readJsonObject(join18(projectRoot, ".mcp.json"));
5755
6245
  const servers = mcp?.mcpServers;
5756
6246
  const configured = servers !== null && typeof servers === "object" && !Array.isArray(servers) && Object.prototype.hasOwnProperty.call(servers, "codegraph");
5757
6247
  return {
5758
6248
  claudeRules,
5759
- hasCodeGraphIndex: await exists4(join16(projectRoot, ".codegraph")),
6249
+ currentClaudeRules,
6250
+ conflictingClaudeRules,
6251
+ hasCodeGraphIndex: await exists4(join18(projectRoot, ".codegraph")),
5760
6252
  codeGraphConfigured: configured
5761
6253
  };
5762
6254
  }
5763
6255
  function destinationTargets(root, surface, name) {
5764
- const stem = basename2(name, extname(name));
6256
+ const stem = basename4(name, extname2(name));
5765
6257
  const targets = [];
5766
- if (surface !== "cli") targets.push(join16(root, ".codebuddy", ".rules", `${stem}.mdc`));
5767
- if (surface !== "ide") targets.push(join16(root, ".codebuddy", "rules", `${stem}.md`));
6258
+ if (surface !== "cli") targets.push(join18(root, ".codebuddy", ".rules", `${stem}.mdc`));
6259
+ if (surface !== "ide") targets.push(join18(root, ".codebuddy", "rules", `${stem}.md`));
5768
6260
  return targets;
5769
6261
  }
5770
6262
  async function applyCodeBuddySetup(options) {
@@ -5776,10 +6268,10 @@ async function applyCodeBuddySetup(options) {
5776
6268
  warnings: []
5777
6269
  };
5778
6270
  if (options.syncClaudeRules) {
5779
- const plan = await inspectCodeBuddySetup(options.projectRoot);
6271
+ const plan = await inspectCodeBuddySetup(options.projectRoot, options.surface);
5780
6272
  for (const name of plan.claudeRules) {
5781
- const source = join16(options.projectRoot, ".claude", "rules", name);
5782
- const content = await readFile15(source, "utf8");
6273
+ const source = join18(options.projectRoot, ".claude", "rules", name);
6274
+ const content = await readFile17(source, "utf8");
5783
6275
  if (SENSITIVE_ASSIGNMENT.test(content) || PRIVATE_KEY_BLOCK.test(content)) {
5784
6276
  result.skippedSensitive.push(name);
5785
6277
  continue;
@@ -5789,14 +6281,14 @@ async function applyCodeBuddySetup(options) {
5789
6281
  result.preserved.push(target);
5790
6282
  continue;
5791
6283
  }
5792
- await mkdir4(dirname4(target), { recursive: true });
5793
- await writeFile6(target, content, { encoding: "utf8", flag: "wx" });
6284
+ await mkdir6(dirname6(target), { recursive: true });
6285
+ await writeFile8(target, content, { encoding: "utf8", flag: "wx" });
5794
6286
  result.copied.push(target);
5795
6287
  }
5796
6288
  }
5797
6289
  }
5798
6290
  if (options.configureCodeGraph) {
5799
- const path = join16(options.projectRoot, ".mcp.json");
6291
+ const path = join18(options.projectRoot, ".mcp.json");
5800
6292
  const current = await readJsonObject(path);
5801
6293
  if (current === null) {
5802
6294
  result.warnings.push(".mcp.json \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u5DF2\u4FDD\u7559\u539F\u6587\u4EF6\u5E76\u8DF3\u8FC7 CodeGraph MCP \u914D\u7F6E");
@@ -5811,7 +6303,7 @@ async function applyCodeBuddySetup(options) {
5811
6303
  codegraph: { command: "codegraph", args: ["serve", "--mcp"] }
5812
6304
  }
5813
6305
  };
5814
- await writeFile6(path, JSON.stringify(next, null, 2) + "\n", "utf8");
6306
+ await writeFile8(path, JSON.stringify(next, null, 2) + "\n", "utf8");
5815
6307
  result.mcpUpdated = true;
5816
6308
  }
5817
6309
  }
@@ -5820,13 +6312,13 @@ async function applyCodeBuddySetup(options) {
5820
6312
  }
5821
6313
 
5822
6314
  // src/commands/refresh.ts
5823
- import { readFile as readFile16 } from "node:fs/promises";
5824
- import { join as join17 } from "node:path";
6315
+ import { readFile as readFile18 } from "node:fs/promises";
6316
+ import { join as join19 } from "node:path";
5825
6317
  import { parse as parseYaml9 } from "yaml";
5826
6318
  async function detectProject(root) {
5827
6319
  let content;
5828
6320
  try {
5829
- content = await readFile16(join17(root, ".harness", "project.yaml"), "utf8");
6321
+ content = await readFile18(join19(root, ".harness", "project.yaml"), "utf8");
5830
6322
  } catch (error) {
5831
6323
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
5832
6324
  return { status: "absent" };
@@ -6055,7 +6547,13 @@ function agentMenuLines(installedProfiles) {
6055
6547
  }
6056
6548
  async function configureCodeBuddyExtras(agents, surface, options, dependencies) {
6057
6549
  if (!agents.includes("codebuddy")) return;
6058
- const plan = await inspectCodeBuddySetup(dependencies.cwd);
6550
+ const plan = await inspectCodeBuddySetup(dependencies.cwd, surface);
6551
+ if (plan.conflictingClaudeRules.length > 0) {
6552
+ dependencies.stderr(
6553
+ `\u4EE5\u4E0B CodeBuddy \u89C4\u5219\u4E0E Claude \u6E90\u89C4\u5219\u5185\u5BB9\u4E0D\u540C\uFF0C\u5DF2\u4FDD\u7559\u76EE\u6807\u6587\u4EF6\uFF1A${plan.conflictingClaudeRules.join(", ")}
6554
+ `
6555
+ );
6556
+ }
6059
6557
  let syncClaudeRules = false;
6060
6558
  let configureCodeGraph = false;
6061
6559
  if (plan.claudeRules.length > 0) {
@@ -6652,12 +7150,94 @@ async function runUpdate(options, dependencies) {
6652
7150
  }
6653
7151
  }
6654
7152
 
7153
+ // src/commands/rules-sync.ts
7154
+ function configuredAgents(config) {
7155
+ const agents = config.adapters.enabled.flatMap((value) => {
7156
+ const parsed = harnessAgentSchema.safeParse(value);
7157
+ return parsed.success ? [parsed.data] : [];
7158
+ });
7159
+ return sortHarnessAgents(agents.length > 0 ? agents : ["claude-code"]);
7160
+ }
7161
+ function configuredSurface(config, override) {
7162
+ const value = override ?? config.adapter_options?.codebuddy?.surface ?? "both";
7163
+ if (value === "both" || value === "ide" || value === "cli") return value;
7164
+ throw new Error("codebuddy surface \u5FC5\u987B\u4E3A both\u3001ide \u6216 cli");
7165
+ }
7166
+ async function runRulesSync(options, dependencies) {
7167
+ const detection = await detectProject(dependencies.cwd);
7168
+ if (detection.status === "absent") {
7169
+ dependencies.stderr("\u5C1A\u672A\u521D\u59CB\u5316 Hunter Harness\uFF1B\u8BF7\u5148\u8FD0\u884C `hunter-harness`\u3002\n");
7170
+ return 3;
7171
+ }
7172
+ if (detection.status === "invalid") {
7173
+ dependencies.stderr("PROJECT_CONFIG_INVALID\uFF1A.harness/project.yaml \u65E0\u6548\n");
7174
+ return 3;
7175
+ }
7176
+ try {
7177
+ const agents = options.agents === void 0 ? configuredAgents(detection.config) : parseAgentsInput(options.agents);
7178
+ const projections = await synchronizeProjectRules(
7179
+ dependencies.cwd,
7180
+ agents,
7181
+ configuredSurface(detection.config, options.codebuddySurface)
7182
+ );
7183
+ const learning = options.learn === false ? null : await synchronizeRuleCandidates(dependencies.cwd);
7184
+ const exitCode = projections.conflicts.length > 0 ? 5 : 0;
7185
+ const payload = {
7186
+ schema_version: 1,
7187
+ command: "rules-sync",
7188
+ request_id: uuidV7(),
7189
+ dry_run: false,
7190
+ ok: exitCode === 0,
7191
+ exit_code: exitCode,
7192
+ project_id: detection.config.project.project_id,
7193
+ summary: {
7194
+ migrated: projections.migrated.length,
7195
+ projected: projections.written.length,
7196
+ removed: projections.removed.length,
7197
+ unchanged: projections.unchanged.length,
7198
+ conflicts: projections.conflicts.length,
7199
+ agent_specific: projections.agent_specific.length,
7200
+ rule_candidates: learning?.candidates ?? 0
7201
+ },
7202
+ items: [
7203
+ ...projections.migrated.map((path) => ({ path, status: "migrated" })),
7204
+ ...projections.written.map((path) => ({ path, status: "projected" })),
7205
+ ...projections.agent_specific.map((path) => ({ path, status: "agent-specific" })),
7206
+ ...learning === null ? [] : [{
7207
+ path: learning.path,
7208
+ status: learning.changed ? "updated" : "unchanged",
7209
+ candidates: learning.candidates,
7210
+ scanned: learning.scanned
7211
+ }]
7212
+ ],
7213
+ warnings: [
7214
+ ...projections.conflicts.map((path) => `\u89C4\u5219\u5206\u6B67\u672A\u8986\u76D6\uFF1A${path}`),
7215
+ ...projections.agent_specific.map((path) => `\u4FDD\u7559 Agent \u4E13\u5C5E\u89C4\u5219\uFF1A${path}`)
7216
+ ],
7217
+ errors: []
7218
+ };
7219
+ if (options.json === true) {
7220
+ dependencies.stdout(serializeCliResult(payload));
7221
+ } else {
7222
+ dependencies.stdout(
7223
+ `\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
7224
+ `
7225
+ );
7226
+ for (const warning of payload.warnings) dependencies.stderr(warning + "\n");
7227
+ }
7228
+ return exitCode;
7229
+ } catch (error) {
7230
+ dependencies.stderr((error instanceof Error ? error.message : String(error)) + "\n");
7231
+ return 1;
7232
+ }
7233
+ }
7234
+
6655
7235
  // src/commands/recovery.ts
6656
- import { stat as stat5 } from "node:fs/promises";
6657
- import { join as join18 } from "node:path";
7236
+ import { stat as stat6 } from "node:fs/promises";
7237
+ import { join as join20 } from "node:path";
6658
7238
  async function exists5(path) {
6659
7239
  try {
6660
- await stat5(path);
7240
+ await stat6(path);
6661
7241
  return true;
6662
7242
  } catch {
6663
7243
  return false;
@@ -6714,7 +7294,7 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
6714
7294
  return 5;
6715
7295
  }
6716
7296
  }
6717
- const initialized = await exists5(join18(dependencies.cwd, ".harness", "project.yaml"));
7297
+ const initialized = await exists5(join20(dependencies.cwd, ".harness", "project.yaml"));
6718
7298
  if (!initialized || options.nonInteractive === true || explicitConfigure(options)) {
6719
7299
  return null;
6720
7300
  }
@@ -6757,8 +7337,8 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
6757
7337
  }
6758
7338
 
6759
7339
  // src/workflow-data/resolve.ts
6760
- import { cp, mkdir as mkdir5, readdir as readdir8, readFile as readFile17, rm as rm8, stat as stat6 } from "node:fs/promises";
6761
- import { dirname as dirname5, join as join19, relative as relative2 } from "node:path";
7340
+ import { cp, mkdir as mkdir7, readdir as readdir10, readFile as readFile19, rm as rm8, stat as stat7 } from "node:fs/promises";
7341
+ import { dirname as dirname7, join as join21, relative as relative3 } from "node:path";
6762
7342
  import { fileURLToPath } from "node:url";
6763
7343
  var WorkflowDataResolutionError = class extends Error {
6764
7344
  code;
@@ -6772,7 +7352,7 @@ var WorkflowDataResolutionError = class extends Error {
6772
7352
  };
6773
7353
  async function pathExists3(path) {
6774
7354
  try {
6775
- await stat6(path);
7355
+ await stat7(path);
6776
7356
  return true;
6777
7357
  } catch {
6778
7358
  return false;
@@ -6792,17 +7372,17 @@ function workflowPackageName(family, env) {
6792
7372
  return `${scope}/workflow-${family}`;
6793
7373
  }
6794
7374
  async function listFilesRecursive(root, base = root) {
6795
- const entries = await readdir8(root, { withFileTypes: true });
7375
+ const entries = await readdir10(root, { withFileTypes: true });
6796
7376
  const files = [];
6797
7377
  for (const entry of entries) {
6798
- const absolute = join19(root, entry.name);
7378
+ const absolute = join21(root, entry.name);
6799
7379
  if (entry.isDirectory()) {
6800
7380
  files.push(...await listFilesRecursive(absolute, base));
6801
7381
  continue;
6802
7382
  }
6803
7383
  if (!entry.isFile()) continue;
6804
- const rel = relative2(base, absolute).replaceAll("\\", "/");
6805
- files.push({ path: rel, content: await readFile17(absolute, "utf8") });
7384
+ const rel = relative3(base, absolute).replaceAll("\\", "/");
7385
+ files.push({ path: rel, content: await readFile19(absolute, "utf8") });
6806
7386
  }
6807
7387
  return files;
6808
7388
  }
@@ -6815,7 +7395,7 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
6815
7395
  const manifest = await readWorkflowFamilyManifest(resourcesRoot);
6816
7396
  const expected = manifest.content_sha256;
6817
7397
  if (typeof expected !== "string" || expected.length === 0) return;
6818
- const harnessRoot = join19(resourcesRoot, "harness");
7398
+ const harnessRoot = join21(resourcesRoot, "harness");
6819
7399
  if (!await pathExists3(harnessRoot)) {
6820
7400
  throw new WorkflowDataResolutionError(
6821
7401
  "\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u7F3A\u5C11 harness/\uFF0C\u65E0\u6CD5\u6821\u9A8C SHA-256",
@@ -6834,40 +7414,40 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
6834
7414
  }
6835
7415
  }
6836
7416
  async function monorepoResourcesRoot() {
6837
- const here = dirname5(fileURLToPath(import.meta.url));
7417
+ const here = dirname7(fileURLToPath(import.meta.url));
6838
7418
  const candidates = [
6839
7419
  // TypeScript source: packages/cli/src/workflow-data -> packages/workflow-data-harness.
6840
- join19(here, "../../../workflow-data-harness"),
7420
+ join21(here, "../../../workflow-data-harness"),
6841
7421
  // Bundled CLI: packages/cli/dist -> packages/workflow-data-harness.
6842
- join19(here, "../../workflow-data-harness"),
7422
+ join21(here, "../../workflow-data-harness"),
6843
7423
  // Test/build layouts that preserve additional source directory levels.
6844
- join19(here, "../../../../packages/workflow-data-harness")
7424
+ join21(here, "../../../../packages/workflow-data-harness")
6845
7425
  ];
6846
7426
  for (const candidate of candidates) {
6847
- if (await pathExists3(join19(candidate, "harness", "manifests"))) return candidate;
7427
+ if (await pathExists3(join21(candidate, "harness", "manifests"))) return candidate;
6848
7428
  }
6849
7429
  return null;
6850
7430
  }
6851
7431
  async function siblingWorkflowPackage(cwd) {
6852
- const here = dirname5(fileURLToPath(import.meta.url));
7432
+ const here = dirname7(fileURLToPath(import.meta.url));
6853
7433
  const candidates = [
6854
- join19(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
7434
+ join21(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
6855
7435
  // Published layout: node_modules/hunter-harness/dist/bin.js alongside the
6856
7436
  // scoped workflow package in node_modules/@hunter-harness/workflow-harness.
6857
- join19(here, "..", "..", "@hunter-harness", "workflow-harness"),
7437
+ join21(here, "..", "..", "@hunter-harness", "workflow-harness"),
6858
7438
  // Monorepo bundled layout: packages/cli/dist/bin.js.
6859
- join19(here, "..", "..", "workflow-data-harness")
7439
+ join21(here, "..", "..", "workflow-data-harness")
6860
7440
  ];
6861
7441
  for (const candidate of candidates) {
6862
- if (await pathExists3(join19(candidate, "harness", "manifests"))) return candidate;
7442
+ if (await pathExists3(join21(candidate, "harness", "manifests"))) return candidate;
6863
7443
  }
6864
7444
  return null;
6865
7445
  }
6866
7446
  async function readCachedNpmPackageVersion(cacheRoot) {
6867
- const manifestPath = join19(cacheRoot, "package.json");
7447
+ const manifestPath = join21(cacheRoot, "package.json");
6868
7448
  if (!await pathExists3(manifestPath)) return null;
6869
7449
  try {
6870
- const pkg = JSON.parse(await readFile17(manifestPath, "utf8"));
7450
+ const pkg = JSON.parse(await readFile19(manifestPath, "utf8"));
6871
7451
  return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : null;
6872
7452
  } catch {
6873
7453
  return null;
@@ -6889,13 +7469,13 @@ async function latestWorkflowCacheIsStale(cacheRoot, packageName, resolveManifes
6889
7469
  }
6890
7470
  }
6891
7471
  async function materializeWorkflowPackageCache(cacheRoot, packageSpec, extract) {
6892
- await mkdir5(cacheRoot, { recursive: true });
6893
- const staging = join19(cacheRoot, ".extract");
7472
+ await mkdir7(cacheRoot, { recursive: true });
7473
+ const staging = join21(cacheRoot, ".extract");
6894
7474
  await rm8(staging, { recursive: true, force: true });
6895
- await mkdir5(staging, { recursive: true });
7475
+ await mkdir7(staging, { recursive: true });
6896
7476
  await extract(packageSpec, staging);
6897
- const packageDir = await pathExists3(join19(staging, "harness", "manifests")) ? staging : join19(staging, "package");
6898
- if (!await pathExists3(join19(packageDir, "harness", "manifests"))) {
7477
+ const packageDir = await pathExists3(join21(staging, "harness", "manifests")) ? staging : join21(staging, "package");
7478
+ if (!await pathExists3(join21(packageDir, "harness", "manifests"))) {
6899
7479
  throw new WorkflowDataResolutionError(
6900
7480
  "\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u5185\u5BB9\u65E0\u6548\uFF1A\u7F3A\u5C11 harness/manifests",
6901
7481
  "WORKFLOW_DATA_INVALID",
@@ -6924,8 +7504,8 @@ async function resolveWorkflowResourcesRoot(options, argv = []) {
6924
7504
  const packageName = workflowPackageName(family, options.env);
6925
7505
  const packageSpec = version === "latest" ? packageName : `${packageName}@${version}`;
6926
7506
  const cacheKey = packageSpec.replace("/", "+");
6927
- const cacheRoot = join19(options.cwd, ".harness", "cache", "workflow-packages", cacheKey);
6928
- if (await pathExists3(join19(cacheRoot, "harness", "manifests"))) {
7507
+ const cacheRoot = join21(options.cwd, ".harness", "cache", "workflow-packages", cacheKey);
7508
+ if (await pathExists3(join21(cacheRoot, "harness", "manifests"))) {
6929
7509
  if (version === "latest") {
6930
7510
  const stale = await latestWorkflowCacheIsStale(cacheRoot, packageName, options.pacoteManifest);
6931
7511
  if (stale) {
@@ -6971,9 +7551,9 @@ function describeWorkflowDataFetchFailure(error, packageSpec) {
6971
7551
  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}`;
6972
7552
  }
6973
7553
  async function readWorkflowFamilyManifest(resourcesRoot) {
6974
- const manifestPath = join19(resourcesRoot, "hunter-workflow-family.json");
7554
+ const manifestPath = join21(resourcesRoot, "hunter-workflow-family.json");
6975
7555
  if (!await pathExists3(manifestPath)) return {};
6976
- return JSON.parse(await readFile17(manifestPath, "utf8"));
7556
+ return JSON.parse(await readFile19(manifestPath, "utf8"));
6977
7557
  }
6978
7558
 
6979
7559
  // src/bin.ts
@@ -7087,6 +7667,12 @@ async function runCli(argv, overrides = {}) {
7087
7667
  dependencies
7088
7668
  );
7089
7669
  });
7670
+ 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) => {
7671
+ exitCode = await runRulesSync(
7672
+ { ...program.opts(), ...options },
7673
+ dependencies
7674
+ );
7675
+ });
7090
7676
  try {
7091
7677
  await program.parseAsync(["node", "hunter-harness", ...argv]);
7092
7678
  return exitCode;