hunter-harness 0.2.29 → 0.2.31

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 (2) hide show
  1. package/dist/bin.js +421 -170
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -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((resolve11) => setTimeout(resolve11, milliseconds));
1236
+ await new Promise((resolve13) => setTimeout(resolve13, milliseconds));
1237
1237
  });
1238
1238
  let lastError;
1239
1239
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
@@ -2290,6 +2290,10 @@ async function readReceipt(root) {
2290
2290
  }
2291
2291
  return { schema_version: 1, source_hashes: {}, targets: {} };
2292
2292
  }
2293
+ async function readManagedProjectRuleProjectionPaths(projectRoot) {
2294
+ const receipt = await readReceipt(resolve3(projectRoot));
2295
+ return new Set(Object.keys(receipt.targets).filter((target) => AGENT_RULE_ROOTS.some((ruleRoot) => target.startsWith(`${ruleRoot}/`))));
2296
+ }
2293
2297
  async function markdownFiles(root) {
2294
2298
  try {
2295
2299
  return (await readdir2(root, { withFileTypes: true })).filter((entry) => entry.isFile() && [".md", ".mdc"].includes(extname(entry.name).toLowerCase())).map((entry) => entry.name).filter((name) => !MANAGED_NAMES.has(name)).sort();
@@ -2305,18 +2309,39 @@ function portable(path) {
2305
2309
  function normalizeRuleContent(content) {
2306
2310
  return content.replace(/\r\n/g, "\n").trimEnd() + "\n";
2307
2311
  }
2308
- function canonicalImportContent(content) {
2312
+ function parseAgentRule(content) {
2309
2313
  const normalized = content.replace(/\r\n/g, "\n");
2310
- if (!normalized.startsWith("---\n"))
2311
- return normalizeRuleContent(normalized);
2314
+ if (!normalized.startsWith("---\n")) {
2315
+ return {
2316
+ canonical: normalizeRuleContent(normalized),
2317
+ globalFrontmatterPrefix: null
2318
+ };
2319
+ }
2312
2320
  const closing = normalized.indexOf("\n---\n", 4);
2313
- if (closing < 0)
2314
- return null;
2321
+ if (closing < 0) {
2322
+ return { canonical: null, globalFrontmatterPrefix: null };
2323
+ }
2315
2324
  const frontmatter = normalized.slice(4, closing);
2316
2325
  if (/^\s*(?:globs?|paths?)\s*:/im.test(frontmatter) || /^\s*alwaysApply\s*:\s*false\s*$/im.test(frontmatter)) {
2317
- return null;
2326
+ return { canonical: null, globalFrontmatterPrefix: null };
2318
2327
  }
2319
- return normalizeRuleContent(normalized.slice(closing + 5));
2328
+ const bodyStart = closing + 5;
2329
+ return {
2330
+ canonical: normalizeRuleContent(normalized.slice(bodyStart).replace(/^\n+/, "")),
2331
+ globalFrontmatterPrefix: normalized.slice(0, bodyStart)
2332
+ };
2333
+ }
2334
+ function canonicalImportContent(content) {
2335
+ return parseAgentRule(content).canonical;
2336
+ }
2337
+ function projectionContent(target, current, canonical) {
2338
+ if (current === null || !target.toLowerCase().endsWith(".mdc"))
2339
+ return canonical;
2340
+ const parsed = parseAgentRule(current);
2341
+ if (parsed.globalFrontmatterPrefix === null)
2342
+ return canonical;
2343
+ return `${parsed.globalFrontmatterPrefix}
2344
+ ${canonical}`;
2320
2345
  }
2321
2346
  async function collectImportCandidates(root, previous, result) {
2322
2347
  const candidates = [];
@@ -2418,9 +2443,10 @@ async function synchronizeProjectRules(projectRoot, agents, surface = "both") {
2418
2443
  result.unchanged.push(target);
2419
2444
  next.targets[target] = current === null ? incomingHash : sha256(current);
2420
2445
  } else if (current === null || previous.targets[target] === sha256(current)) {
2421
- await atomicWrite(path, content);
2446
+ const projected = projectionContent(target, current, content);
2447
+ await atomicWrite(path, projected);
2422
2448
  result.written.push(target);
2423
- next.targets[target] = incomingHash;
2449
+ next.targets[target] = sha256(projected);
2424
2450
  } else {
2425
2451
  result.conflicts.push(target);
2426
2452
  next.targets[target] = previous.targets[target] ?? sha256(current);
@@ -3205,16 +3231,157 @@ async function synchronizeRuleCandidates(projectRoot, options = {}) {
3205
3231
  };
3206
3232
  }
3207
3233
 
3208
- // ../core/dist/project/refresh.js
3234
+ // ../core/dist/project/rule-review.js
3209
3235
  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";
3236
+ import { mkdir as mkdir6, readFile as readFile7, rename as rename5, writeFile as writeFile6 } from "node:fs/promises";
3237
+ import { basename as basename4, dirname as dirname5, join as join8, relative as relative2, resolve as resolve7 } from "node:path";
3238
+ var CANDIDATE_PATH2 = ".harness/knowledge/rule-candidates.json";
3239
+ var DECISION_PATH = ".harness/knowledge/rule-decisions.json";
3240
+ var RULES_ROOT2 = ".harness/rules";
3241
+ function sha2563(content) {
3242
+ return createHash7("sha256").update(content).digest("hex");
3243
+ }
3244
+ async function readOptional2(path) {
3245
+ try {
3246
+ return await readFile7(path, "utf8");
3247
+ } catch (error) {
3248
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
3249
+ return null;
3250
+ throw error;
3251
+ }
3252
+ }
3253
+ async function readJson(path, fallback) {
3254
+ const content = await readOptional2(path);
3255
+ if (content === null)
3256
+ return fallback;
3257
+ return JSON.parse(content);
3258
+ }
3259
+ async function atomicWrite3(path, content) {
3260
+ await mkdir6(dirname5(path), { recursive: true });
3261
+ const temporary = `${path}.${process.pid}.tmp`;
3262
+ await writeFile6(temporary, content, "utf8");
3263
+ await rename5(temporary, path);
3264
+ }
3265
+ function candidateRevision(candidate) {
3266
+ return sha2563(JSON.stringify(candidate));
3267
+ }
3268
+ function portable3(path) {
3269
+ return path.replaceAll("\\", "/");
3270
+ }
3271
+ function ruleTarget2(root, input) {
3272
+ const portablePath = portable3(input);
3273
+ if (!portablePath.startsWith(`${RULES_ROOT2}/`) || !portablePath.toLowerCase().endsWith(".md") || basename4(portablePath) === "") {
3274
+ throw new Error(`RULE_PATCH_PATH_INVALID: ${input}`);
3275
+ }
3276
+ const target = resolve7(root, ...portablePath.split("/"));
3277
+ const rulesRoot = resolve7(root, ...RULES_ROOT2.split("/"));
3278
+ const relativePath = portable3(relative2(rulesRoot, target));
3279
+ if (relativePath === ".." || relativePath.startsWith("../")) {
3280
+ throw new Error(`RULE_PATCH_PATH_INVALID: ${input}`);
3281
+ }
3282
+ return target;
3283
+ }
3284
+ function validateDecision(decision, candidates) {
3285
+ const candidate = candidates.get(decision.candidate_id);
3286
+ if (candidate === void 0) {
3287
+ throw new Error(`RULE_CANDIDATE_UNKNOWN: ${decision.candidate_id}`);
3288
+ }
3289
+ if (candidateRevision(candidate) !== decision.candidate_revision) {
3290
+ throw new Error(`RULE_CANDIDATE_STALE: ${decision.candidate_id}`);
3291
+ }
3292
+ if (decision.dispositions.length === 0 || decision.reason.trim().length < 8) {
3293
+ throw new Error(`RULE_DECISION_INVALID: ${decision.candidate_id}`);
3294
+ }
3295
+ if (decision.dispositions.includes("defer") && decision.review_after === void 0) {
3296
+ throw new Error(`RULE_DECISION_REVIEW_AFTER_REQUIRED: ${decision.candidate_id}`);
3297
+ }
3298
+ if (decision.dispositions.includes("public-rule") !== (decision.rule_patch !== void 0)) {
3299
+ throw new Error(`RULE_PATCH_REQUIRED: ${decision.candidate_id}`);
3300
+ }
3301
+ return candidate;
3302
+ }
3303
+ async function readCandidates(root) {
3304
+ return readJson(join8(root, ...CANDIDATE_PATH2.split("/")), { schema_version: 1, source_hashes: {}, candidates: [] });
3305
+ }
3306
+ async function readDecisions(root) {
3307
+ return readJson(join8(root, ...DECISION_PATH.split("/")), { schema_version: 1, decisions: [] });
3308
+ }
3309
+ async function exportRuleReviewQueue(projectRoot) {
3310
+ const root = resolve7(projectRoot);
3311
+ const candidates = await readCandidates(root);
3312
+ const decisions = await readDecisions(root);
3313
+ const decided = new Set(decisions.decisions.map((decision) => `${decision.candidate_id}\0${decision.candidate_revision}`));
3314
+ const pending = candidates.candidates.flatMap((candidate) => {
3315
+ const revision = candidateRevision(candidate);
3316
+ return decided.has(`${candidate.id}\0${revision}`) ? [] : [{
3317
+ candidate_id: candidate.id,
3318
+ candidate_revision: revision,
3319
+ candidate
3320
+ }];
3321
+ });
3322
+ return {
3323
+ schema_version: 1,
3324
+ pending,
3325
+ decided: candidates.candidates.length - pending.length
3326
+ };
3327
+ }
3328
+ async function applyRuleReviewDecisions(projectRoot, input) {
3329
+ if (input.schema_version !== 1 || !Array.isArray(input.decisions)) {
3330
+ throw new Error("RULE_DECISIONS_INVALID");
3331
+ }
3332
+ const root = resolve7(projectRoot);
3333
+ const candidateManifest = await readCandidates(root);
3334
+ const candidates = new Map(candidateManifest.candidates.map((candidate) => [
3335
+ candidate.id,
3336
+ candidate
3337
+ ]));
3338
+ const plannedPatches = [];
3339
+ for (const decision of input.decisions) {
3340
+ validateDecision(decision, candidates);
3341
+ if (decision.rule_patch === void 0)
3342
+ continue;
3343
+ const target = ruleTarget2(root, decision.rule_patch.target_path);
3344
+ const current = await readOptional2(target);
3345
+ const currentHash = current === null ? null : sha2563(current);
3346
+ if (currentHash !== decision.rule_patch.expected_sha256) {
3347
+ throw new Error(`RULE_PATCH_STALE: ${decision.rule_patch.target_path}`);
3348
+ }
3349
+ plannedPatches.push({ path: target, content: decision.rule_patch.content });
3350
+ }
3351
+ const currentManifest = await readDecisions(root);
3352
+ const replacements = new Map(input.decisions.map((decision) => [
3353
+ `${decision.candidate_id}\0${decision.candidate_revision}`,
3354
+ decision
3355
+ ]));
3356
+ for (const decision of currentManifest.decisions) {
3357
+ const key = `${decision.candidate_id}\0${decision.candidate_revision}`;
3358
+ if (!replacements.has(key))
3359
+ replacements.set(key, decision);
3360
+ }
3361
+ const next = {
3362
+ schema_version: 1,
3363
+ decisions: [...replacements.values()].sort((left, right) => left.candidate_id.localeCompare(right.candidate_id) || left.candidate_revision.localeCompare(right.candidate_revision))
3364
+ };
3365
+ for (const patch of plannedPatches)
3366
+ await atomicWrite3(patch.path, patch.content);
3367
+ await atomicWrite3(join8(root, ...DECISION_PATH.split("/")), JSON.stringify(next, null, 2) + "\n");
3368
+ return {
3369
+ applied: plannedPatches.length,
3370
+ recorded: input.decisions.length,
3371
+ path: DECISION_PATH
3372
+ };
3373
+ }
3374
+
3375
+ // ../core/dist/project/refresh.js
3376
+ import { createHash as createHash8 } from "node:crypto";
3377
+ import { readFile as readFile8, readdir as readdir5, rmdir } from "node:fs/promises";
3378
+ import { dirname as dirname6, join as join9, resolve as resolve8 } from "node:path";
3212
3379
  import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
3213
3380
  var INSTALLED_STATE_PATH = ".harness/state/local/installed-harness-bundle.json";
3214
3381
  var CONTEXT_INDEX_PATH2 = ".harness/context-index.json";
3215
3382
  async function fileHex2(path) {
3216
3383
  try {
3217
- return createHash7("sha256").update(await readFile7(path)).digest("hex");
3384
+ return createHash8("sha256").update(await readFile8(path)).digest("hex");
3218
3385
  } catch (error) {
3219
3386
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
3220
3387
  return null;
@@ -3224,7 +3391,7 @@ async function fileHex2(path) {
3224
3391
  }
3225
3392
  async function readOptionalText(path) {
3226
3393
  try {
3227
- return await readFile7(path, "utf8");
3394
+ return await readFile8(path, "utf8");
3228
3395
  } catch (error) {
3229
3396
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
3230
3397
  return "";
@@ -3233,7 +3400,7 @@ async function readOptionalText(path) {
3233
3400
  }
3234
3401
  }
3235
3402
  async function readInstalledState(root) {
3236
- const content = await readOptionalText(join8(root, INSTALLED_STATE_PATH));
3403
+ const content = await readOptionalText(join9(root, INSTALLED_STATE_PATH));
3237
3404
  if (content === "") {
3238
3405
  return {
3239
3406
  profile: null,
@@ -3303,7 +3470,7 @@ async function readInstalledState(root) {
3303
3470
  };
3304
3471
  }
3305
3472
  async function readInstalledAgentConfiguration(projectRoot) {
3306
- const installed = await readInstalledState(resolve7(projectRoot));
3473
+ const installed = await readInstalledState(resolve8(projectRoot));
3307
3474
  return {
3308
3475
  agents: installed.adapters,
3309
3476
  profiles: Object.fromEntries(installed.adapters.map((agent) => [
@@ -3313,7 +3480,7 @@ async function readInstalledAgentConfiguration(projectRoot) {
3313
3480
  };
3314
3481
  }
3315
3482
  async function readContextIndexBundleHash(root) {
3316
- const content = await readOptionalText(join8(root, CONTEXT_INDEX_PATH2));
3483
+ const content = await readOptionalText(join9(root, CONTEXT_INDEX_PATH2));
3317
3484
  if (content === "")
3318
3485
  return null;
3319
3486
  try {
@@ -3325,9 +3492,9 @@ async function readContextIndexBundleHash(root) {
3325
3492
  }
3326
3493
  }
3327
3494
  async function pruneEmptyParentDirs(root, deletedPaths, boundaryPaths) {
3328
- const boundaries = new Set(boundaryPaths.map((path) => join8(root, path)));
3495
+ const boundaries = new Set(boundaryPaths.map((path) => join9(root, path)));
3329
3496
  for (const deleted of deletedPaths) {
3330
- let dir = dirname5(join8(root, deleted));
3497
+ let dir = dirname6(join9(root, deleted));
3331
3498
  while (dir.startsWith(root) && !boundaries.has(dir)) {
3332
3499
  let entries;
3333
3500
  try {
@@ -3342,7 +3509,7 @@ async function pruneEmptyParentDirs(root, deletedPaths, boundaryPaths) {
3342
3509
  } catch {
3343
3510
  break;
3344
3511
  }
3345
- dir = dirname5(dir);
3512
+ dir = dirname6(dir);
3346
3513
  }
3347
3514
  }
3348
3515
  }
@@ -3369,7 +3536,7 @@ function sortByTarget(items) {
3369
3536
  return [...items].sort((left, right) => left.target_path.localeCompare(right.target_path));
3370
3537
  }
3371
3538
  async function reconcileContextIndex(root, agents, profiles, manifests, codebuddySurface2, verifications) {
3372
- const existing = await readOptionalText(join8(root, CONTEXT_INDEX_PATH2));
3539
+ const existing = await readOptionalText(join9(root, CONTEXT_INDEX_PATH2));
3373
3540
  let codebase = {
3374
3541
  map: ".harness/codebase/map",
3375
3542
  status: "missing"
@@ -3422,7 +3589,7 @@ async function reconcileContextIndex(root, agents, profiles, manifests, codebudd
3422
3589
  }
3423
3590
  async function projectTransitionOperation(root, agents, profiles, codebuddySurface2) {
3424
3591
  const path = ".harness/project.yaml";
3425
- const content = await readOptionalText(join8(root, path));
3592
+ const content = await readOptionalText(join9(root, path));
3426
3593
  if (content === "")
3427
3594
  return null;
3428
3595
  const project = parseYaml3(content);
@@ -3457,11 +3624,11 @@ function mergeTargets(targets) {
3457
3624
  }).sort((left, right) => left.target_path.localeCompare(right.target_path));
3458
3625
  }
3459
3626
  async function reconcileMarkdownBlock(root, fileName, blockId, content, remove, ops, conflicts, preserved) {
3460
- const original = await readOptionalText(join8(root, fileName));
3627
+ const original = await readOptionalText(join9(root, fileName));
3461
3628
  const synthetic = {
3462
3629
  source_path: fileName,
3463
3630
  target_path: fileName,
3464
- sha256: createHash7("sha256").update(content).digest("hex"),
3631
+ sha256: createHash8("sha256").update(content).digest("hex"),
3465
3632
  bytes: new TextEncoder().encode(content)
3466
3633
  };
3467
3634
  let next;
@@ -3476,7 +3643,7 @@ async function reconcileMarkdownBlock(root, fileName, blockId, content, remove,
3476
3643
  next = refreshed.content;
3477
3644
  }
3478
3645
  } catch {
3479
- const current = original === "" ? null : createHash7("sha256").update(original).digest("hex");
3646
+ const current = original === "" ? null : createHash8("sha256").update(original).digest("hex");
3480
3647
  preserved.push(item(synthetic, "preserve", "MALFORMED_MANAGED_BLOCK", current, synthetic.sha256));
3481
3648
  conflicts.push(conflict(synthetic, "MALFORMED_MANAGED_BLOCK", current, synthetic.sha256));
3482
3649
  return;
@@ -3496,7 +3663,7 @@ function stateWithoutInstalledAt(value) {
3496
3663
  return copy;
3497
3664
  }
3498
3665
  async function refreshProject(options) {
3499
- const root = resolve7(options.projectRoot);
3666
+ const root = resolve8(options.projectRoot);
3500
3667
  const installed = await readInstalledState(root);
3501
3668
  const oldAgents = installed.adapters.length > 0 ? installed.adapters : ["claude-code"];
3502
3669
  const selectedAgents = sortHarnessAgents(options.agents);
@@ -3580,7 +3747,7 @@ async function refreshProject(options) {
3580
3747
  const newStateFiles = installed.files.filter((entry) => entry.owner === "shared" || !selectedSet.has(entry.owner));
3581
3748
  for (const target of newManaged) {
3582
3749
  const incoming = target.sha256;
3583
- const current = await fileHex2(join8(root, target.target_path));
3750
+ const current = await fileHex2(join9(root, target.target_path));
3584
3751
  if (current === null) {
3585
3752
  applied.push(item(target, "add", "MISSING_TARGET", null, incoming));
3586
3753
  ops.push({ operation: "add", path: target.target_path, content: target.bytes });
@@ -3608,7 +3775,7 @@ async function refreshProject(options) {
3608
3775
  }
3609
3776
  }
3610
3777
  for (const target of oldOnly) {
3611
- const current = await fileHex2(join8(root, target.target_path));
3778
+ const current = await fileHex2(join9(root, target.target_path));
3612
3779
  if (current === null) {
3613
3780
  continue;
3614
3781
  }
@@ -3651,7 +3818,7 @@ async function refreshProject(options) {
3651
3818
  const verifyEntries = [];
3652
3819
  for (const target of verifyTargets) {
3653
3820
  const rel = target.target_path.replace(/\\/g, "/");
3654
- const actual = await fileHex2(join8(root, target.target_path));
3821
+ const actual = await fileHex2(join9(root, target.target_path));
3655
3822
  verifyEntries.push({ relpath: rel, sha256: actual ?? "" });
3656
3823
  if (actual === null) {
3657
3824
  verifyMismatches.push({ relpath: rel, expected: target.sha256, actual: "<missing>" });
@@ -3684,19 +3851,19 @@ async function refreshProject(options) {
3684
3851
  owner: "shared",
3685
3852
  target_path: "AGENTS.md",
3686
3853
  block_id: AGENTS_CORE_BLOCK_ID,
3687
- content_sha256: createHash7("sha256").update(AGENTS_MANAGED_BLOCK_CONTENT).digest("hex")
3854
+ content_sha256: createHash8("sha256").update(AGENTS_MANAGED_BLOCK_CONTENT).digest("hex")
3688
3855
  },
3689
3856
  ...selectedSet.has("claude-code") ? [{
3690
3857
  owner: "claude-code",
3691
3858
  target_path: "CLAUDE.md",
3692
3859
  block_id: CLAUDE_BLOCK_ID,
3693
- content_sha256: createHash7("sha256").update(CLAUDE_MANAGED_BLOCK_CONTENT).digest("hex")
3860
+ content_sha256: createHash8("sha256").update(CLAUDE_MANAGED_BLOCK_CONTENT).digest("hex")
3694
3861
  }] : [],
3695
3862
  ...selectedSet.has("codebuddy") ? [{
3696
3863
  owner: "codebuddy",
3697
3864
  target_path: "CODEBUDDY.md",
3698
3865
  block_id: CODEBUDDY_BLOCK_ID,
3699
- content_sha256: createHash7("sha256").update(CODEBUDDY_MANAGED_BLOCK_CONTENT).digest("hex")
3866
+ content_sha256: createHash8("sha256").update(CODEBUDDY_MANAGED_BLOCK_CONTENT).digest("hex")
3700
3867
  }] : []
3701
3868
  ].sort((left, right) => left.target_path.localeCompare(right.target_path) || left.block_id.localeCompare(right.block_id));
3702
3869
  const filesByTarget = new Map(newStateFiles.map((entry) => [entry.target_path, entry]));
@@ -3712,7 +3879,7 @@ async function refreshProject(options) {
3712
3879
  files: [...filesByTarget.values()].sort((left, right) => left.target_path.localeCompare(right.target_path) || left.source_path.localeCompare(right.source_path)),
3713
3880
  managed_blocks: managedBlocks
3714
3881
  };
3715
- const existingState = await readOptionalText(join8(root, INSTALLED_STATE_PATH));
3882
+ const existingState = await readOptionalText(join9(root, INSTALLED_STATE_PATH));
3716
3883
  let existingParsed = null;
3717
3884
  try {
3718
3885
  existingParsed = existingState === "" ? null : JSON.parse(existingState);
@@ -3767,7 +3934,7 @@ function buildMarkerCoreHash(text) {
3767
3934
  }
3768
3935
  }
3769
3936
  async function collectFreshness(options) {
3770
- const root = resolve7(options.projectRoot);
3937
+ const root = resolve8(options.projectRoot);
3771
3938
  const installed = await readInstalledState(root);
3772
3939
  const codebuddySurface2 = options.codebuddySurface ?? "both";
3773
3940
  const agents = [];
@@ -3812,18 +3979,18 @@ async function collectFreshness(options) {
3812
3979
  identity.adapterHash = sha256Bytes(canonicalJson(targets.map((target) => ({ path: target.target_path.replace(/\\/g, "/"), sha256: target.sha256 })).sort((a, b) => a.path.localeCompare(b.path))));
3813
3980
  const installedProjection = await Promise.all(targets.map(async (target) => ({
3814
3981
  path: target.target_path.replace(/\\/g, "/"),
3815
- sha256: await fileHex2(join8(root, target.target_path))
3982
+ sha256: await fileHex2(join9(root, target.target_path))
3816
3983
  })));
3817
3984
  identity.installedAdapterHash = sha256Bytes(canonicalJson(installedProjection.sort((a, b) => a.path.localeCompare(b.path))));
3818
3985
  const markerTarget = targets.find((target) => target.target_path.replace(/\\/g, "/").endsWith(`/${BUILD_MARKER_BUNDLE_PATH}`) || target.target_path === BUILD_MARKER_BUNDLE_PATH);
3819
3986
  if (markerTarget !== void 0) {
3820
- identity.installedCoreHash = buildMarkerCoreHash(await readOptionalText(join8(root, markerTarget.target_path)));
3987
+ identity.installedCoreHash = buildMarkerCoreHash(await readOptionalText(join9(root, markerTarget.target_path)));
3821
3988
  }
3822
3989
  const mismatchDetails = [];
3823
3990
  const contentEntries = [];
3824
3991
  for (const target of targets) {
3825
3992
  const rel = target.target_path.replace(/\\/g, "/");
3826
- const actual = await fileHex2(join8(root, target.target_path));
3993
+ const actual = await fileHex2(join9(root, target.target_path));
3827
3994
  contentEntries.push({ relpath: rel, sha256: actual ?? "" });
3828
3995
  if (actual === null) {
3829
3996
  mismatchDetails.push({ relpath: rel, expected: target.sha256, actual: "<missing>" });
@@ -3846,7 +4013,7 @@ async function collectFreshness(options) {
3846
4013
  const drifted = [];
3847
4014
  const missing = [];
3848
4015
  for (const target of targets) {
3849
- const current = await fileHex2(join8(root, target.target_path));
4016
+ const current = await fileHex2(join9(root, target.target_path));
3850
4017
  if (current === null) {
3851
4018
  missing.push(target.target_path);
3852
4019
  } else if (current !== target.sha256) {
@@ -4094,11 +4261,11 @@ function rawFindings(content) {
4094
4261
  if (rule.id === "HH_PASSWORD_VALUE" && isObviousPlaceholder(value)) {
4095
4262
  continue;
4096
4263
  }
4097
- const relative4 = match[0].indexOf(value);
4264
+ const relative5 = match[0].indexOf(value);
4098
4265
  findings.push({
4099
4266
  ruleId: rule.id,
4100
4267
  severity: rule.severity,
4101
- offset: match.index + Math.max(0, relative4),
4268
+ offset: match.index + Math.max(0, relative5),
4102
4269
  value
4103
4270
  });
4104
4271
  }
@@ -4210,8 +4377,8 @@ function generateProposalPreview(input, scanOptions = {}) {
4210
4377
  }
4211
4378
 
4212
4379
  // ../core/dist/push/credentials.js
4213
- import { readFile as readFile8, writeFile as writeFile6 } from "node:fs/promises";
4214
- import { join as join9 } from "node:path";
4380
+ import { readFile as readFile9, writeFile as writeFile7 } from "node:fs/promises";
4381
+ import { join as join10 } from "node:path";
4215
4382
  import { parse as parseYaml4, stringify as stringifyYaml3 } from "yaml";
4216
4383
  var CREDENTIALS_LOCAL_RELATIVE = ".harness/credentials.local.yaml";
4217
4384
  var CREDENTIALS_GITIGNORE_LINES = [
@@ -4281,7 +4448,7 @@ function mergeLocalCredentials(existing, patch) {
4281
4448
  }
4282
4449
  async function readLocalCredentials(projectRoot) {
4283
4450
  try {
4284
- const raw = await readFile8(join9(projectRoot, CREDENTIALS_LOCAL_RELATIVE), "utf8");
4451
+ const raw = await readFile9(join10(projectRoot, CREDENTIALS_LOCAL_RELATIVE), "utf8");
4285
4452
  return parseLocalCredentials(parseYaml4(raw));
4286
4453
  } catch (error) {
4287
4454
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -4292,13 +4459,13 @@ async function readLocalCredentials(projectRoot) {
4292
4459
  }
4293
4460
  async function writeLocalCredentials(projectRoot, credentials) {
4294
4461
  const normalized = validateLocalCredentials(credentials);
4295
- await writeFile6(join9(projectRoot, CREDENTIALS_LOCAL_RELATIVE), stringifyYaml3(normalized, { sortMapEntries: true }) + "\n", "utf8");
4462
+ await writeFile7(join10(projectRoot, CREDENTIALS_LOCAL_RELATIVE), stringifyYaml3(normalized, { sortMapEntries: true }) + "\n", "utf8");
4296
4463
  }
4297
4464
  async function ensureCredentialsGitignore(projectRoot) {
4298
- const gitignorePath = join9(projectRoot, ".gitignore");
4465
+ const gitignorePath = join10(projectRoot, ".gitignore");
4299
4466
  let content = "";
4300
4467
  try {
4301
- content = await readFile8(gitignorePath, "utf8");
4468
+ content = await readFile9(gitignorePath, "utf8");
4302
4469
  } catch (error) {
4303
4470
  if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
4304
4471
  throw error;
@@ -4321,7 +4488,7 @@ async function ensureCredentialsGitignore(projectRoot) {
4321
4488
  return;
4322
4489
  }
4323
4490
  const suffix = content.length === 0 || content.endsWith("\n") ? "" : "\n";
4324
- await writeFile6(gitignorePath, content + suffix + missing.join("\n") + "\n", "utf8");
4491
+ await writeFile7(gitignorePath, content + suffix + missing.join("\n") + "\n", "utf8");
4325
4492
  }
4326
4493
  function resolvePushAuth(input) {
4327
4494
  const tokenEnv = input.tokenEnv ?? input.projectTokenEnv;
@@ -4349,28 +4516,28 @@ function resolvePushAuth(input) {
4349
4516
  }
4350
4517
 
4351
4518
  // ../core/dist/push/push.js
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";
4519
+ import { lstat as lstat3, readFile as readFile13, readdir as readdir6, rm as rm5 } from "node:fs/promises";
4520
+ import { join as join14, relative as relative3, resolve as resolve10 } from "node:path";
4354
4521
  import { parse as parseYaml5, stringify as stringifyYaml4 } from "yaml";
4355
4522
 
4356
4523
  // ../core/dist/state/baseline.js
4357
- import { readFile as readFile9 } from "node:fs/promises";
4358
- import { join as join10 } from "node:path";
4524
+ import { readFile as readFile10 } from "node:fs/promises";
4525
+ import { join as join11 } from "node:path";
4359
4526
  async function readBaseline(projectRoot) {
4360
- const content = await readFile9(join10(stateLayout(projectRoot).baseline, "manifest.json"), "utf8");
4527
+ const content = await readFile10(join11(stateLayout(projectRoot).baseline, "manifest.json"), "utf8");
4361
4528
  return baselineManifestSchema.parse(JSON.parse(content));
4362
4529
  }
4363
4530
 
4364
4531
  // ../core/dist/state/locks.js
4365
4532
  import { randomUUID as randomUUID3 } from "node:crypto";
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";
4533
+ import { readFile as readFile11, rename as rename6, rm as rm3, writeFile as writeFile8 } from "node:fs/promises";
4534
+ import { join as join12 } from "node:path";
4368
4535
  async function readLock(path) {
4369
- return JSON.parse(await readFile10(path, "utf8"));
4536
+ return JSON.parse(await readFile11(path, "utf8"));
4370
4537
  }
4371
4538
  async function acquireProtocolLock(projectRoot, operation, options = {}) {
4372
4539
  const layout = await ensureStateLayout(projectRoot);
4373
- const lockPath = join11(layout.locks, "protocol.lock");
4540
+ const lockPath = join12(layout.locks, "protocol.lock");
4374
4541
  const now = options.now ?? Date.now();
4375
4542
  const staleAfterMs = options.staleAfterMs ?? 15 * 60 * 1e3;
4376
4543
  const record = {
@@ -4383,7 +4550,7 @@ async function acquireProtocolLock(projectRoot, operation, options = {}) {
4383
4550
  };
4384
4551
  for (let attempt = 0; attempt < 2; attempt += 1) {
4385
4552
  try {
4386
- await writeFile7(lockPath, JSON.stringify(record, null, 2), { flag: "wx" });
4553
+ await writeFile8(lockPath, JSON.stringify(record, null, 2), { flag: "wx" });
4387
4554
  return {
4388
4555
  path: lockPath,
4389
4556
  operation,
@@ -4405,15 +4572,15 @@ async function acquireProtocolLock(projectRoot, operation, options = {}) {
4405
4572
  if (now - current.heartbeat_at_ms <= staleAfterMs) {
4406
4573
  throw new Error("protocol lock is active for operation " + current.operation, { cause: error });
4407
4574
  }
4408
- await rename5(lockPath, lockPath + ".stale-" + randomUUID3());
4575
+ await rename6(lockPath, lockPath + ".stale-" + randomUUID3());
4409
4576
  }
4410
4577
  }
4411
4578
  throw new Error("unable to acquire protocol lock");
4412
4579
  }
4413
4580
 
4414
4581
  // ../core/dist/sync/synchronize.js
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";
4582
+ import { lstat as lstat2, readFile as readFile12, rm as rm4 } from "node:fs/promises";
4583
+ import { join as join13, resolve as resolve9 } from "node:path";
4417
4584
 
4418
4585
  // ../core/dist/update/conflicts.js
4419
4586
  function operationTargetPath(operation) {
@@ -4591,7 +4758,8 @@ function planArtifactRebase(input) {
4591
4758
  plan.alreadyApplied.push({ path: target, operation });
4592
4759
  continue;
4593
4760
  }
4594
- const staticDecision = decideUpdate(policy, false);
4761
+ const projectionManaged = input.protocolOnlyPaths?.has(source) === true || input.protocolOnlyPaths?.has(target) === true;
4762
+ const staticDecision = projectionManaged ? { apply: false, reason: "protocol-only" } : decideUpdate(policy, false);
4595
4763
  if (!staticDecision.apply) {
4596
4764
  const acknowledgeReason = staticDecision.reason === "protocol-only" ? "protocol-only" : "policy-never";
4597
4765
  const entry2 = acknowledgedEntry(operation, context, input.projectVersion, acknowledgeReason);
@@ -4695,8 +4863,8 @@ async function pathExists(path) {
4695
4863
  }
4696
4864
  }
4697
4865
  async function optionalContent(root, path) {
4698
- const full = join12(root, path);
4699
- return await pathExists(full) ? readFile11(full, "utf8") : null;
4866
+ const full = join13(root, path);
4867
+ return await pathExists(full) ? readFile12(full, "utf8") : null;
4700
4868
  }
4701
4869
  function manifestPayloadHash(manifest) {
4702
4870
  const payload = { ...manifest };
@@ -4708,10 +4876,10 @@ async function loadBlob(root, client, artifactId, operation, requestId, dryRun)
4708
4876
  return null;
4709
4877
  }
4710
4878
  const hash = operation.content_sha256;
4711
- const cacheRoot = join12(root, ".harness", "cache", "server-artifacts", artifactId);
4712
- const cachePath = join12(cacheRoot, "blobs", hash.replace(":", "_"));
4879
+ const cacheRoot = join13(root, ".harness", "cache", "server-artifacts", artifactId);
4880
+ const cachePath = join13(cacheRoot, "blobs", hash.replace(":", "_"));
4713
4881
  if (await pathExists(cachePath) && await sha256File(cachePath) === hash) {
4714
- return readFile11(cachePath, "utf8");
4882
+ return readFile12(cachePath, "utf8");
4715
4883
  }
4716
4884
  const bytes = await client.downloadArtifactBlob(artifactId, hash, requestId);
4717
4885
  if (bytes.byteLength !== operation.size_bytes || sha256Bytes(bytes) !== hash) {
@@ -4802,7 +4970,7 @@ function applyBaselineUpdates(baseline, plan) {
4802
4970
  }
4803
4971
  return next;
4804
4972
  }
4805
- async function planSingleArtifact(root, baseline, manifest, client, requestId, dryRun, conflictStrategy, resolveOverrides) {
4973
+ async function planSingleArtifact(root, baseline, manifest, client, requestId, dryRun, conflictStrategy, resolveOverrides, protocolOnlyPaths) {
4806
4974
  if (manifest.project_version === null) {
4807
4975
  throw new Error("artifact manifest missing project_version");
4808
4976
  }
@@ -4812,11 +4980,12 @@ async function planSingleArtifact(root, baseline, manifest, client, requestId, d
4812
4980
  projectVersion: manifest.project_version,
4813
4981
  contexts,
4814
4982
  conflictStrategy,
4815
- ...resolveOverrides === void 0 ? {} : { resolveOverrides }
4983
+ ...resolveOverrides === void 0 ? {} : { resolveOverrides },
4984
+ ...protocolOnlyPaths === void 0 ? {} : { protocolOnlyPaths }
4816
4985
  });
4817
4986
  }
4818
4987
  async function saveConflictReport(root, requestId, manifest, plan) {
4819
- const reportPath = join12(root, ".harness", "reports", "conflicts-" + requestId + ".json");
4988
+ const reportPath = join13(root, ".harness", "reports", "conflicts-" + requestId + ".json");
4820
4989
  await atomicWriteJson(reportPath, {
4821
4990
  schema_version: 1,
4822
4991
  request_id: requestId,
@@ -4827,7 +4996,7 @@ async function saveConflictReport(root, requestId, manifest, plan) {
4827
4996
  });
4828
4997
  }
4829
4998
  async function synchronizeArtifacts(options, initialBaseline) {
4830
- const root = resolve8(options.projectRoot);
4999
+ const root = resolve9(options.projectRoot);
4831
5000
  const projectId = options.project.project.project_id;
4832
5001
  if (projectId === null) {
4833
5002
  throw new Error("PROJECT_NOT_BOUND");
@@ -4876,7 +5045,7 @@ async function synchronizeArtifacts(options, initialBaseline) {
4876
5045
  if (manifest.artifact_id !== discovery.artifact_id || manifest.project_id !== projectId || manifest.project_version === null || manifestPayloadHash(manifest) !== manifest.manifest_sha256) {
4877
5046
  throw new Error("ARTIFACT_HASH_MISMATCH");
4878
5047
  }
4879
- let plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, conflictStrategy, options.resolveOverrides);
5048
+ let plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, conflictStrategy, options.resolveOverrides, options.protocolOnlyPaths);
4880
5049
  if (plan.conflicts.length > 0 && options.confirmConflictStrategy !== void 0) {
4881
5050
  const confirmed = await options.confirmConflictStrategy(plan.conflicts);
4882
5051
  if (confirmed === false) {
@@ -4890,7 +5059,7 @@ async function synchronizeArtifacts(options, initialBaseline) {
4890
5059
  aggregate.artifactId = manifest.artifact_id;
4891
5060
  return aggregate;
4892
5061
  }
4893
- plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, confirmed, options.resolveOverrides);
5062
+ plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, confirmed, options.resolveOverrides, options.protocolOnlyPaths);
4894
5063
  }
4895
5064
  const paths = pathsFromPlan(plan);
4896
5065
  aggregate.applied.push(...paths.applied);
@@ -4934,7 +5103,7 @@ async function synchronizeArtifacts(options, initialBaseline) {
4934
5103
  }
4935
5104
  continue;
4936
5105
  }
4937
- await atomicWriteJson(join12(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
5106
+ await atomicWriteJson(join13(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
4938
5107
  const lock = await acquireProtocolLock(root, "update", { requestId: options.requestId });
4939
5108
  try {
4940
5109
  const nextBaseline = applyBaselineUpdates(baseline, plan);
@@ -5004,7 +5173,7 @@ function baselineEntryFromOperation(manifest, operation, localContent) {
5004
5173
  };
5005
5174
  }
5006
5175
  async function advanceBaselineFromArtifact(options, baseline) {
5007
- const root = resolve8(options.projectRoot);
5176
+ const root = resolve9(options.projectRoot);
5008
5177
  if (options.manifest.project_version === null) {
5009
5178
  throw new Error("artifact manifest missing project_version");
5010
5179
  }
@@ -5106,19 +5275,19 @@ async function walkFiles(root, current, output) {
5106
5275
  return;
5107
5276
  }
5108
5277
  for (const item2 of await readdir6(current, { withFileTypes: true })) {
5109
- const path = join13(current, item2.name);
5278
+ const path = join14(current, item2.name);
5110
5279
  if (item2.isSymbolicLink()) {
5111
5280
  throw new PushWorkflowError("symlink is not pushable", 6, "UNSAFE_SYMLINK");
5112
5281
  }
5113
5282
  if (item2.isDirectory()) {
5114
5283
  await walkFiles(root, path, output);
5115
5284
  } else if (item2.isFile()) {
5116
- output.push(normalizeManagedPath(relative2(root, path).replaceAll("\\", "/")));
5285
+ output.push(normalizeManagedPath(relative3(root, path).replaceAll("\\", "/")));
5117
5286
  }
5118
5287
  }
5119
5288
  }
5120
5289
  async function walkArchiveSummaries(root, output) {
5121
- const archiveRoot = join13(root, ".harness", "archive");
5290
+ const archiveRoot = join14(root, ".harness", "archive");
5122
5291
  if (!await exists3(archiveRoot))
5123
5292
  return;
5124
5293
  for (const item2 of await readdir6(archiveRoot, { withFileTypes: true })) {
@@ -5127,7 +5296,7 @@ async function walkArchiveSummaries(root, output) {
5127
5296
  }
5128
5297
  if (!item2.isDirectory())
5129
5298
  continue;
5130
- const summaryPath = join13(archiveRoot, item2.name, "reports", "final", "summary-data.json");
5299
+ const summaryPath = join14(archiveRoot, item2.name, "reports", "final", "summary-data.json");
5131
5300
  try {
5132
5301
  const stats = await lstat3(summaryPath);
5133
5302
  if (stats.isSymbolicLink()) {
@@ -5140,7 +5309,7 @@ async function walkArchiveSummaries(root, output) {
5140
5309
  continue;
5141
5310
  throw error;
5142
5311
  }
5143
- const relativePath = normalizeManagedPath(relative2(root, summaryPath).replaceAll("\\", "/"));
5312
+ const relativePath = normalizeManagedPath(relative3(root, summaryPath).replaceAll("\\", "/"));
5144
5313
  if (ARCHIVE_SUMMARY_PATH.test(relativePath)) {
5145
5314
  output.push(relativePath);
5146
5315
  }
@@ -5157,17 +5326,17 @@ async function walkHarnessEntries(root, directory, output) {
5157
5326
  return;
5158
5327
  for (const item2 of await readdir6(directory, { withFileTypes: true })) {
5159
5328
  if (item2.name.startsWith("harness-")) {
5160
- const path = join13(directory, item2.name);
5329
+ const path = join14(directory, item2.name);
5161
5330
  if (item2.isDirectory()) {
5162
5331
  await walkFiles(root, path, output);
5163
5332
  } else if (item2.isFile()) {
5164
- output.push(normalizeManagedPath(relative2(root, path).replaceAll("\\", "/")));
5333
+ output.push(normalizeManagedPath(relative3(root, path).replaceAll("\\", "/")));
5165
5334
  }
5166
5335
  }
5167
5336
  }
5168
5337
  }
5169
5338
  async function managedFiles(projectRoot, project) {
5170
- const root = resolve9(projectRoot);
5339
+ const root = resolve10(projectRoot);
5171
5340
  const paths = [];
5172
5341
  const adapters = getAdapters(enabledHarnessAgents(project));
5173
5342
  const managedFiles2 = [
@@ -5176,26 +5345,26 @@ async function managedFiles(projectRoot, project) {
5176
5345
  ...adapters.some((adapter) => adapter.name === "codebuddy") ? ["CODEBUDDY.md"] : []
5177
5346
  ];
5178
5347
  for (const path of managedFiles2) {
5179
- if (await exists3(join13(root, path))) {
5348
+ if (await exists3(join14(root, path))) {
5180
5349
  paths.push(path);
5181
5350
  }
5182
5351
  }
5183
5352
  for (const path of SHARED_MANAGED_ROOTS) {
5184
- await walkFiles(root, join13(root, path), paths);
5353
+ await walkFiles(root, join14(root, path), paths);
5185
5354
  }
5186
5355
  await walkArchiveSummaries(root, paths);
5187
5356
  for (const adapter of adapters) {
5188
5357
  if (adapter.rulesRoot !== null) {
5189
- await walkHarnessEntries(root, join13(root, adapter.rulesRoot), paths);
5358
+ await walkHarnessEntries(root, join14(root, adapter.rulesRoot), paths);
5190
5359
  }
5191
- await walkHarnessEntries(root, join13(root, adapter.skillsRoot), paths);
5360
+ await walkHarnessEntries(root, join14(root, adapter.skillsRoot), paths);
5192
5361
  if (adapter.agentsRoot !== null) {
5193
- await walkHarnessEntries(root, join13(root, adapter.agentsRoot), paths);
5362
+ await walkHarnessEntries(root, join14(root, adapter.agentsRoot), paths);
5194
5363
  }
5195
5364
  }
5196
5365
  const result = {};
5197
5366
  for (const path of [...new Set(paths)].sort()) {
5198
- result[path] = await readFile12(join13(root, path), "utf8");
5367
+ result[path] = await readFile13(join14(root, path), "utf8");
5199
5368
  }
5200
5369
  return result;
5201
5370
  }
@@ -5204,14 +5373,14 @@ function proposalBaseline(baseline) {
5204
5373
  }
5205
5374
  async function readProject(root) {
5206
5375
  try {
5207
- return projectConfigSchema.parse(parseYaml5(await readFile12(join13(root, ".harness", "project.yaml"), "utf8")));
5376
+ return projectConfigSchema.parse(parseYaml5(await readFile13(join14(root, ".harness", "project.yaml"), "utf8")));
5208
5377
  } catch {
5209
5378
  throw new PushWorkflowError("project configuration is missing or invalid", 3, "PROJECT_CONFIG_INVALID");
5210
5379
  }
5211
5380
  }
5212
5381
  async function readOptionalJson(path) {
5213
5382
  try {
5214
- return JSON.parse(await readFile12(path, "utf8"));
5383
+ return JSON.parse(await readFile13(path, "utf8"));
5215
5384
  } catch (error) {
5216
5385
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
5217
5386
  return null;
@@ -5223,7 +5392,7 @@ async function clientIdFor(root, explicit) {
5223
5392
  if (explicit !== void 0) {
5224
5393
  return explicit;
5225
5394
  }
5226
- const path = join13(root, ".harness", "state", "local", "client.json");
5395
+ const path = join14(root, ".harness", "state", "local", "client.json");
5227
5396
  const existing = await readOptionalJson(path);
5228
5397
  if (typeof existing?.client_id === "string" && /^cli_[A-Za-z0-9_-]+$/.test(existing.client_id)) {
5229
5398
  return existing.client_id;
@@ -5419,7 +5588,7 @@ async function bindProject(root, project, baseline, projectId) {
5419
5588
  return { project: nextProject, baseline: nextBaseline };
5420
5589
  }
5421
5590
  async function pushProject(options) {
5422
- const root = resolve9(options.projectRoot);
5591
+ const root = resolve10(options.projectRoot);
5423
5592
  let project = await readProject(root);
5424
5593
  let baseline = await readBaseline(root);
5425
5594
  const profile = parseHarnessProfile(project.project.profiles[0]);
@@ -5485,7 +5654,7 @@ async function pushProject(options) {
5485
5654
  if (options.confirmProposal !== void 0 && !await options.confirmProposal(preview)) {
5486
5655
  return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
5487
5656
  }
5488
- const workflowPath = join13(root, ".harness", "state", "local", "push-workflow.json");
5657
+ const workflowPath = join14(root, ".harness", "state", "local", "push-workflow.json");
5489
5658
  const priorWorkflow = await readOptionalJson(workflowPath);
5490
5659
  const provisionalRequestId = priorWorkflow?.local_project_key === project.project.local_project_key ? priorWorkflow.request_id : uuidV7();
5491
5660
  const lock = await acquireProtocolLock(root, "push", { requestId: provisionalRequestId });
@@ -5640,7 +5809,7 @@ async function pushProject(options) {
5640
5809
  pushWarning = "BASELINE_ADVANCE_DEFERRED";
5641
5810
  }
5642
5811
  }
5643
- await atomicWriteJson(join13(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
5812
+ await atomicWriteJson(join14(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
5644
5813
  schema_version: 1,
5645
5814
  request_id: requestId,
5646
5815
  project_id: projectId,
@@ -5685,8 +5854,8 @@ async function pushProject(options) {
5685
5854
  }
5686
5855
 
5687
5856
  // ../core/dist/state/cleanup.js
5688
- import { readFile as readFile13, readdir as readdir7, rm as rm6 } from "node:fs/promises";
5689
- import { join as join14 } from "node:path";
5857
+ import { readFile as readFile14, readdir as readdir7, rm as rm6 } from "node:fs/promises";
5858
+ import { join as join15 } from "node:path";
5690
5859
  function isSafeEntryName(name) {
5691
5860
  return name.length > 0 && !name.includes("/") && !name.includes("\\") && name !== "." && name !== ".." && !name.includes("\0");
5692
5861
  }
@@ -5710,7 +5879,7 @@ async function cleanupProject(options) {
5710
5879
  continue;
5711
5880
  let journal;
5712
5881
  try {
5713
- journal = JSON.parse(await readFile13(join14(layout.transactions, name, "journal.json"), "utf8"));
5882
+ journal = JSON.parse(await readFile14(join15(layout.transactions, name, "journal.json"), "utf8"));
5714
5883
  } catch {
5715
5884
  continue;
5716
5885
  }
@@ -5728,7 +5897,7 @@ async function cleanupProject(options) {
5728
5897
  continue;
5729
5898
  pruned.push(entry.id);
5730
5899
  if (!options.dryRun) {
5731
- await rm6(join14(layout.transactions, entry.id), { recursive: true, force: true });
5900
+ await rm6(join15(layout.transactions, entry.id), { recursive: true, force: true });
5732
5901
  }
5733
5902
  }
5734
5903
  }
@@ -5737,7 +5906,7 @@ async function cleanupProject(options) {
5737
5906
  continue;
5738
5907
  removedCache.push(name);
5739
5908
  if (!options.dryRun) {
5740
- await rm6(join14(layout.serverArtifacts, name), { recursive: true, force: true });
5909
+ await rm6(join15(layout.serverArtifacts, name), { recursive: true, force: true });
5741
5910
  }
5742
5911
  }
5743
5912
  return {
@@ -5797,10 +5966,10 @@ var SKILL_TARGET_AGENTS = Object.freeze([
5797
5966
  ]);
5798
5967
 
5799
5968
  // ../core/dist/transaction/recovery.js
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";
5969
+ import { readFile as readFile15, readdir as readdir8, rm as rm7, stat as stat4 } from "node:fs/promises";
5970
+ import { join as join16 } from "node:path";
5802
5971
  async function recoverTransaction(projectRoot, transactionId) {
5803
- const journal = JSON.parse(await readFile14(join15(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
5972
+ const journal = JSON.parse(await readFile15(join16(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
5804
5973
  if (journal.state === "committed") {
5805
5974
  return { transactionId, status: "committed" };
5806
5975
  }
@@ -5827,7 +5996,7 @@ async function listTransactions(projectRoot) {
5827
5996
  const transactions = [];
5828
5997
  for (const transactionId of names) {
5829
5998
  try {
5830
- const journal = JSON.parse(await readFile14(join15(root, transactionId, "journal.json"), "utf8"));
5999
+ const journal = JSON.parse(await readFile15(join16(root, transactionId, "journal.json"), "utf8"));
5831
6000
  transactions.push({
5832
6001
  transactionId,
5833
6002
  kind: journal.kind,
@@ -5858,11 +6027,11 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
5858
6027
  if (latest === void 0) {
5859
6028
  throw new Error("no committed update transaction is available for rollback");
5860
6029
  }
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"));
6030
+ const transactionRoot = join16(stateLayout(projectRoot).transactions, latest.transactionId);
6031
+ const journal = JSON.parse(await readFile15(join16(transactionRoot, "journal.json"), "utf8"));
6032
+ const after = JSON.parse(await readFile15(join16(transactionRoot, "after", "manifest.json"), "utf8"));
5864
6033
  for (const entry of after) {
5865
- const target = join15(projectRoot, entry.path);
6034
+ const target = join16(projectRoot, entry.path);
5866
6035
  const exists6 = await pathExists2(target);
5867
6036
  if (exists6 !== entry.exists || exists6 && await sha256File(target) !== entry.hash) {
5868
6037
  throw new Error("cannot rollback dirty path: " + entry.path);
@@ -5875,10 +6044,10 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
5875
6044
  continue;
5876
6045
  }
5877
6046
  seen.add(snapshot.path);
5878
- const target = join15(projectRoot, snapshot.path);
6047
+ const target = join16(projectRoot, snapshot.path);
5879
6048
  const exists6 = await pathExists2(target);
5880
6049
  if (snapshot.existed && snapshot.snapshot_name !== null) {
5881
- const content = await readFile14(join15(transactionRoot, "before", snapshot.snapshot_name));
6050
+ const content = await readFile15(join16(transactionRoot, "before", snapshot.snapshot_name));
5882
6051
  operations.push({
5883
6052
  operation: exists6 ? "modify" : "add",
5884
6053
  path: snapshot.path,
@@ -5907,7 +6076,7 @@ async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Dat
5907
6076
  if (!removable) {
5908
6077
  continue;
5909
6078
  }
5910
- await rm7(join15(stateLayout(projectRoot).transactions, item2.transactionId), {
6079
+ await rm7(join16(stateLayout(projectRoot).transactions, item2.transactionId), {
5911
6080
  recursive: true,
5912
6081
  force: true
5913
6082
  });
@@ -5917,8 +6086,8 @@ async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Dat
5917
6086
  }
5918
6087
 
5919
6088
  // ../core/dist/update/update.js
5920
- import { readFile as readFile15 } from "node:fs/promises";
5921
- import { join as join16, resolve as resolve10 } from "node:path";
6089
+ import { readFile as readFile16 } from "node:fs/promises";
6090
+ import { join as join17, resolve as resolve11 } from "node:path";
5922
6091
  import { parse as parseYaml8 } from "yaml";
5923
6092
  var UpdateWorkflowError = class extends Error {
5924
6093
  exitCode;
@@ -5931,10 +6100,10 @@ var UpdateWorkflowError = class extends Error {
5931
6100
  }
5932
6101
  };
5933
6102
  async function updateProject(options) {
5934
- const root = resolve10(options.projectRoot);
6103
+ const root = resolve11(options.projectRoot);
5935
6104
  let project;
5936
6105
  try {
5937
- project = projectConfigSchema.parse(parseYaml8(await readFile15(join16(root, ".harness", "project.yaml"), "utf8")));
6106
+ project = projectConfigSchema.parse(parseYaml8(await readFile16(join17(root, ".harness", "project.yaml"), "utf8")));
5938
6107
  } catch {
5939
6108
  throw new UpdateWorkflowError("project configuration is invalid", 3, "PROJECT_CONFIG_INVALID");
5940
6109
  }
@@ -5958,6 +6127,7 @@ async function updateProject(options) {
5958
6127
  }
5959
6128
  const requestId = uuidV7();
5960
6129
  const baseline = await readBaseline(root);
6130
+ const protocolOnlyPaths = await readManagedProjectRuleProjectionPaths(root);
5961
6131
  let parsedServerUrl;
5962
6132
  try {
5963
6133
  parsedServerUrl = new URL(serverUrl);
@@ -5979,6 +6149,7 @@ async function updateProject(options) {
5979
6149
  client,
5980
6150
  requestId,
5981
6151
  dryRun: options.dryRun,
6152
+ protocolOnlyPaths,
5982
6153
  ...options.conflictStrategy === void 0 ? {} : { conflictStrategy: options.conflictStrategy },
5983
6154
  ...options.resolveOverrides === void 0 ? {} : { resolveOverrides: options.resolveOverrides },
5984
6155
  ...options.confirmConflictStrategy === void 0 ? {} : { confirmConflictStrategy: options.confirmConflictStrategy },
@@ -5989,7 +6160,13 @@ async function updateProject(options) {
5989
6160
  const parsed = harnessAgentSchema.safeParse(agent);
5990
6161
  return parsed.success ? [parsed.data] : [];
5991
6162
  });
5992
- await synchronizeProjectRules(root, agents, project.adapter_options?.codebuddy?.surface ?? "both");
6163
+ const projections = await synchronizeProjectRules(root, agents, project.adapter_options?.codebuddy?.surface ?? "both");
6164
+ for (const path of projections.conflicts) {
6165
+ result.conflicts.push({ path, operation: "modify", reason: "local-dirty" });
6166
+ if (!result.skipped.some((item2) => item2.path === path)) {
6167
+ result.skipped.push({ path, operation: "modify", reason: "local-dirty" });
6168
+ }
6169
+ }
5993
6170
  }
5994
6171
  return result;
5995
6172
  } catch (error) {
@@ -6016,8 +6193,8 @@ async function updateProject(options) {
6016
6193
  }
6017
6194
 
6018
6195
  // src/config/init-config.ts
6019
- import { isAbsolute as isAbsolute2, join as join17 } from "node:path";
6020
- import { readFile as readFile16 } from "node:fs/promises";
6196
+ import { isAbsolute as isAbsolute2, join as join18 } from "node:path";
6197
+ import { readFile as readFile17 } from "node:fs/promises";
6021
6198
  var InitConfigurationError = class extends Error {
6022
6199
  exitCode;
6023
6200
  code;
@@ -6100,9 +6277,9 @@ function hasOwn(record, key) {
6100
6277
  async function resolveInitConfig(cwd, flags, prompts = {}, warnings = []) {
6101
6278
  let fileConfig = {};
6102
6279
  if (flags.config !== void 0) {
6103
- const path = isAbsolute2(flags.config) ? flags.config : join17(cwd, flags.config);
6280
+ const path = isAbsolute2(flags.config) ? flags.config : join18(cwd, flags.config);
6104
6281
  try {
6105
- fileConfig = JSON.parse(await readFile16(path, "utf8"));
6282
+ fileConfig = JSON.parse(await readFile17(path, "utf8"));
6106
6283
  } catch (error) {
6107
6284
  throw new InitConfigurationError(
6108
6285
  "unable to read init config: " + (error instanceof Error ? error.message : String(error)),
@@ -6184,8 +6361,8 @@ function serializeCliResult(result) {
6184
6361
  }
6185
6362
 
6186
6363
  // src/config/codebuddy-setup.ts
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";
6364
+ import { mkdir as mkdir7, readFile as readFile18, readdir as readdir9, stat as stat5, writeFile as writeFile9 } from "node:fs/promises";
6365
+ import { basename as basename5, dirname as dirname7, extname as extname2, join as join19 } from "node:path";
6189
6366
  var MANAGED_RULE_NAMES = /* @__PURE__ */ new Set([
6190
6367
  "harness-general.md",
6191
6368
  "harness-general.mdc",
@@ -6204,7 +6381,7 @@ async function exists4(path) {
6204
6381
  }
6205
6382
  async function readJsonObject(path) {
6206
6383
  try {
6207
- const parsed = JSON.parse(await readFile17(path, "utf8"));
6384
+ const parsed = JSON.parse(await readFile18(path, "utf8"));
6208
6385
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
6209
6386
  } catch (error) {
6210
6387
  if (error instanceof Error && "code" in error && error.code === "ENOENT") return {};
@@ -6212,7 +6389,7 @@ async function readJsonObject(path) {
6212
6389
  }
6213
6390
  }
6214
6391
  async function inspectCodeBuddySetup(projectRoot, surface = "both") {
6215
- const rulesRoot = join18(projectRoot, ".claude", "rules");
6392
+ const rulesRoot = join19(projectRoot, ".claude", "rules");
6216
6393
  let ruleNames = [];
6217
6394
  try {
6218
6395
  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();
@@ -6223,11 +6400,11 @@ async function inspectCodeBuddySetup(projectRoot, surface = "both") {
6223
6400
  const currentClaudeRules = [];
6224
6401
  const conflictingClaudeRules = [];
6225
6402
  for (const name of ruleNames) {
6226
- const sourceContent = await readFile17(join18(rulesRoot, name), "utf8");
6403
+ const sourceContent = await readFile18(join19(rulesRoot, name), "utf8");
6227
6404
  const targetContents = await Promise.all(
6228
6405
  destinationTargets(projectRoot, surface, name).map(async (target) => {
6229
6406
  try {
6230
- return await readFile17(target, "utf8");
6407
+ return await readFile18(target, "utf8");
6231
6408
  } catch (error) {
6232
6409
  if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
6233
6410
  throw error;
@@ -6241,22 +6418,22 @@ async function inspectCodeBuddySetup(projectRoot, surface = "both") {
6241
6418
  currentClaudeRules.push(name);
6242
6419
  }
6243
6420
  }
6244
- const mcp = await readJsonObject(join18(projectRoot, ".mcp.json"));
6421
+ const mcp = await readJsonObject(join19(projectRoot, ".mcp.json"));
6245
6422
  const servers = mcp?.mcpServers;
6246
6423
  const configured = servers !== null && typeof servers === "object" && !Array.isArray(servers) && Object.prototype.hasOwnProperty.call(servers, "codegraph");
6247
6424
  return {
6248
6425
  claudeRules,
6249
6426
  currentClaudeRules,
6250
6427
  conflictingClaudeRules,
6251
- hasCodeGraphIndex: await exists4(join18(projectRoot, ".codegraph")),
6428
+ hasCodeGraphIndex: await exists4(join19(projectRoot, ".codegraph")),
6252
6429
  codeGraphConfigured: configured
6253
6430
  };
6254
6431
  }
6255
6432
  function destinationTargets(root, surface, name) {
6256
- const stem = basename4(name, extname2(name));
6433
+ const stem = basename5(name, extname2(name));
6257
6434
  const targets = [];
6258
- if (surface !== "cli") targets.push(join18(root, ".codebuddy", ".rules", `${stem}.mdc`));
6259
- if (surface !== "ide") targets.push(join18(root, ".codebuddy", "rules", `${stem}.md`));
6435
+ if (surface !== "cli") targets.push(join19(root, ".codebuddy", ".rules", `${stem}.mdc`));
6436
+ if (surface !== "ide") targets.push(join19(root, ".codebuddy", "rules", `${stem}.md`));
6260
6437
  return targets;
6261
6438
  }
6262
6439
  async function applyCodeBuddySetup(options) {
@@ -6270,8 +6447,8 @@ async function applyCodeBuddySetup(options) {
6270
6447
  if (options.syncClaudeRules) {
6271
6448
  const plan = await inspectCodeBuddySetup(options.projectRoot, options.surface);
6272
6449
  for (const name of plan.claudeRules) {
6273
- const source = join18(options.projectRoot, ".claude", "rules", name);
6274
- const content = await readFile17(source, "utf8");
6450
+ const source = join19(options.projectRoot, ".claude", "rules", name);
6451
+ const content = await readFile18(source, "utf8");
6275
6452
  if (SENSITIVE_ASSIGNMENT.test(content) || PRIVATE_KEY_BLOCK.test(content)) {
6276
6453
  result.skippedSensitive.push(name);
6277
6454
  continue;
@@ -6281,14 +6458,14 @@ async function applyCodeBuddySetup(options) {
6281
6458
  result.preserved.push(target);
6282
6459
  continue;
6283
6460
  }
6284
- await mkdir6(dirname6(target), { recursive: true });
6285
- await writeFile8(target, content, { encoding: "utf8", flag: "wx" });
6461
+ await mkdir7(dirname7(target), { recursive: true });
6462
+ await writeFile9(target, content, { encoding: "utf8", flag: "wx" });
6286
6463
  result.copied.push(target);
6287
6464
  }
6288
6465
  }
6289
6466
  }
6290
6467
  if (options.configureCodeGraph) {
6291
- const path = join18(options.projectRoot, ".mcp.json");
6468
+ const path = join19(options.projectRoot, ".mcp.json");
6292
6469
  const current = await readJsonObject(path);
6293
6470
  if (current === null) {
6294
6471
  result.warnings.push(".mcp.json \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u5DF2\u4FDD\u7559\u539F\u6587\u4EF6\u5E76\u8DF3\u8FC7 CodeGraph MCP \u914D\u7F6E");
@@ -6303,7 +6480,7 @@ async function applyCodeBuddySetup(options) {
6303
6480
  codegraph: { command: "codegraph", args: ["serve", "--mcp"] }
6304
6481
  }
6305
6482
  };
6306
- await writeFile8(path, JSON.stringify(next, null, 2) + "\n", "utf8");
6483
+ await writeFile9(path, JSON.stringify(next, null, 2) + "\n", "utf8");
6307
6484
  result.mcpUpdated = true;
6308
6485
  }
6309
6486
  }
@@ -6312,13 +6489,13 @@ async function applyCodeBuddySetup(options) {
6312
6489
  }
6313
6490
 
6314
6491
  // src/commands/refresh.ts
6315
- import { readFile as readFile18 } from "node:fs/promises";
6316
- import { join as join19 } from "node:path";
6492
+ import { readFile as readFile19 } from "node:fs/promises";
6493
+ import { join as join20 } from "node:path";
6317
6494
  import { parse as parseYaml9 } from "yaml";
6318
6495
  async function detectProject(root) {
6319
6496
  let content;
6320
6497
  try {
6321
- content = await readFile18(join19(root, ".harness", "project.yaml"), "utf8");
6498
+ content = await readFile19(join20(root, ".harness", "project.yaml"), "utf8");
6322
6499
  } catch (error) {
6323
6500
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
6324
6501
  return { status: "absent" };
@@ -7181,6 +7358,7 @@ async function runRulesSync(options, dependencies) {
7181
7358
  configuredSurface(detection.config, options.codebuddySurface)
7182
7359
  );
7183
7360
  const learning = options.learn === false ? null : await synchronizeRuleCandidates(dependencies.cwd);
7361
+ const ruleReview = learning === null ? null : await exportRuleReviewQueue(dependencies.cwd);
7184
7362
  const exitCode = projections.conflicts.length > 0 ? 5 : 0;
7185
7363
  const payload = {
7186
7364
  schema_version: 1,
@@ -7197,7 +7375,8 @@ async function runRulesSync(options, dependencies) {
7197
7375
  unchanged: projections.unchanged.length,
7198
7376
  conflicts: projections.conflicts.length,
7199
7377
  agent_specific: projections.agent_specific.length,
7200
- rule_candidates: learning?.candidates ?? 0
7378
+ rule_candidates: learning?.candidates ?? 0,
7379
+ rule_review_pending: ruleReview?.pending.length ?? 0
7201
7380
  },
7202
7381
  items: [
7203
7382
  ...projections.migrated.map((path) => ({ path, status: "migrated" })),
@@ -7208,6 +7387,12 @@ async function runRulesSync(options, dependencies) {
7208
7387
  status: learning.changed ? "updated" : "unchanged",
7209
7388
  candidates: learning.candidates,
7210
7389
  scanned: learning.scanned
7390
+ }],
7391
+ ...ruleReview === null ? [] : [{
7392
+ path: ".harness/knowledge/rule-decisions.json",
7393
+ status: ruleReview.pending.length > 0 ? "pending-review" : "current",
7394
+ pending: ruleReview.pending.length,
7395
+ decided: ruleReview.decided
7211
7396
  }]
7212
7397
  ],
7213
7398
  warnings: [
@@ -7232,9 +7417,69 @@ async function runRulesSync(options, dependencies) {
7232
7417
  }
7233
7418
  }
7234
7419
 
7420
+ // src/commands/rules-review.ts
7421
+ import { readFile as readFile20 } from "node:fs/promises";
7422
+ import { resolve as resolve12 } from "node:path";
7423
+ async function runRulesReview(options, dependencies) {
7424
+ const detection = await detectProject(dependencies.cwd);
7425
+ if (detection.status === "absent") {
7426
+ dependencies.stderr("\u5C1A\u672A\u521D\u59CB\u5316 Hunter Harness\uFF1B\u8BF7\u5148\u8FD0\u884C `hunter-harness`\u3002\n");
7427
+ return 3;
7428
+ }
7429
+ if (detection.status === "invalid") {
7430
+ dependencies.stderr("PROJECT_CONFIG_INVALID\uFF1A.harness/project.yaml \u65E0\u6548\n");
7431
+ return 3;
7432
+ }
7433
+ try {
7434
+ const queue = await exportRuleReviewQueue(dependencies.cwd);
7435
+ let summary;
7436
+ let items;
7437
+ if (options.apply === void 0) {
7438
+ summary = { pending: queue.pending.length, decided: queue.decided };
7439
+ items = queue.pending;
7440
+ } else {
7441
+ const input = JSON.parse(
7442
+ await readFile20(resolve12(dependencies.cwd, options.apply), "utf8")
7443
+ );
7444
+ const result = await applyRuleReviewDecisions(dependencies.cwd, input);
7445
+ summary = { applied: result.applied, recorded: result.recorded };
7446
+ items = [{ path: result.path, status: "updated" }];
7447
+ }
7448
+ const payload = {
7449
+ schema_version: 1,
7450
+ command: "rules-review",
7451
+ request_id: uuidV7(),
7452
+ dry_run: false,
7453
+ ok: true,
7454
+ exit_code: 0,
7455
+ project_id: detection.config.project.project_id,
7456
+ summary,
7457
+ items,
7458
+ warnings: [],
7459
+ errors: []
7460
+ };
7461
+ if (options.json === true) dependencies.stdout(serializeCliResult(payload));
7462
+ else if (options.apply === void 0) {
7463
+ dependencies.stdout(
7464
+ `\u516C\u5171\u89C4\u5219\u5019\u9009\uFF1A\u5F85\u8BC4\u5BA1 ${summary.pending ?? 0}\uFF0C\u5DF2\u51B3\u5B9A ${summary.decided ?? 0}\u3002
7465
+ `
7466
+ );
7467
+ } else {
7468
+ dependencies.stdout(
7469
+ `\u516C\u5171\u89C4\u5219\u51B3\u7B56\uFF1A\u5E94\u7528 ${summary.applied ?? 0}\uFF0C\u8BB0\u5F55 ${summary.recorded ?? 0}\u3002
7470
+ `
7471
+ );
7472
+ }
7473
+ return 0;
7474
+ } catch (error) {
7475
+ dependencies.stderr((error instanceof Error ? error.message : String(error)) + "\n");
7476
+ return 1;
7477
+ }
7478
+ }
7479
+
7235
7480
  // src/commands/recovery.ts
7236
7481
  import { stat as stat6 } from "node:fs/promises";
7237
- import { join as join20 } from "node:path";
7482
+ import { join as join21 } from "node:path";
7238
7483
  async function exists5(path) {
7239
7484
  try {
7240
7485
  await stat6(path);
@@ -7294,7 +7539,7 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
7294
7539
  return 5;
7295
7540
  }
7296
7541
  }
7297
- const initialized = await exists5(join20(dependencies.cwd, ".harness", "project.yaml"));
7542
+ const initialized = await exists5(join21(dependencies.cwd, ".harness", "project.yaml"));
7298
7543
  if (!initialized || options.nonInteractive === true || explicitConfigure(options)) {
7299
7544
  return null;
7300
7545
  }
@@ -7337,8 +7582,8 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
7337
7582
  }
7338
7583
 
7339
7584
  // src/workflow-data/resolve.ts
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";
7585
+ import { cp, mkdir as mkdir8, readdir as readdir10, readFile as readFile21, rm as rm8, stat as stat7 } from "node:fs/promises";
7586
+ import { dirname as dirname8, join as join22, relative as relative4 } from "node:path";
7342
7587
  import { fileURLToPath } from "node:url";
7343
7588
  var WorkflowDataResolutionError = class extends Error {
7344
7589
  code;
@@ -7375,14 +7620,14 @@ async function listFilesRecursive(root, base = root) {
7375
7620
  const entries = await readdir10(root, { withFileTypes: true });
7376
7621
  const files = [];
7377
7622
  for (const entry of entries) {
7378
- const absolute = join21(root, entry.name);
7623
+ const absolute = join22(root, entry.name);
7379
7624
  if (entry.isDirectory()) {
7380
7625
  files.push(...await listFilesRecursive(absolute, base));
7381
7626
  continue;
7382
7627
  }
7383
7628
  if (!entry.isFile()) continue;
7384
- const rel = relative3(base, absolute).replaceAll("\\", "/");
7385
- files.push({ path: rel, content: await readFile19(absolute, "utf8") });
7629
+ const rel = relative4(base, absolute).replaceAll("\\", "/");
7630
+ files.push({ path: rel, content: await readFile21(absolute, "utf8") });
7386
7631
  }
7387
7632
  return files;
7388
7633
  }
@@ -7395,7 +7640,7 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
7395
7640
  const manifest = await readWorkflowFamilyManifest(resourcesRoot);
7396
7641
  const expected = manifest.content_sha256;
7397
7642
  if (typeof expected !== "string" || expected.length === 0) return;
7398
- const harnessRoot = join21(resourcesRoot, "harness");
7643
+ const harnessRoot = join22(resourcesRoot, "harness");
7399
7644
  if (!await pathExists3(harnessRoot)) {
7400
7645
  throw new WorkflowDataResolutionError(
7401
7646
  "\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u7F3A\u5C11 harness/\uFF0C\u65E0\u6CD5\u6821\u9A8C SHA-256",
@@ -7414,40 +7659,40 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
7414
7659
  }
7415
7660
  }
7416
7661
  async function monorepoResourcesRoot() {
7417
- const here = dirname7(fileURLToPath(import.meta.url));
7662
+ const here = dirname8(fileURLToPath(import.meta.url));
7418
7663
  const candidates = [
7419
7664
  // TypeScript source: packages/cli/src/workflow-data -> packages/workflow-data-harness.
7420
- join21(here, "../../../workflow-data-harness"),
7665
+ join22(here, "../../../workflow-data-harness"),
7421
7666
  // Bundled CLI: packages/cli/dist -> packages/workflow-data-harness.
7422
- join21(here, "../../workflow-data-harness"),
7667
+ join22(here, "../../workflow-data-harness"),
7423
7668
  // Test/build layouts that preserve additional source directory levels.
7424
- join21(here, "../../../../packages/workflow-data-harness")
7669
+ join22(here, "../../../../packages/workflow-data-harness")
7425
7670
  ];
7426
7671
  for (const candidate of candidates) {
7427
- if (await pathExists3(join21(candidate, "harness", "manifests"))) return candidate;
7672
+ if (await pathExists3(join22(candidate, "harness", "manifests"))) return candidate;
7428
7673
  }
7429
7674
  return null;
7430
7675
  }
7431
7676
  async function siblingWorkflowPackage(cwd) {
7432
- const here = dirname7(fileURLToPath(import.meta.url));
7677
+ const here = dirname8(fileURLToPath(import.meta.url));
7433
7678
  const candidates = [
7434
- join21(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
7679
+ join22(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
7435
7680
  // Published layout: node_modules/hunter-harness/dist/bin.js alongside the
7436
7681
  // scoped workflow package in node_modules/@hunter-harness/workflow-harness.
7437
- join21(here, "..", "..", "@hunter-harness", "workflow-harness"),
7682
+ join22(here, "..", "..", "@hunter-harness", "workflow-harness"),
7438
7683
  // Monorepo bundled layout: packages/cli/dist/bin.js.
7439
- join21(here, "..", "..", "workflow-data-harness")
7684
+ join22(here, "..", "..", "workflow-data-harness")
7440
7685
  ];
7441
7686
  for (const candidate of candidates) {
7442
- if (await pathExists3(join21(candidate, "harness", "manifests"))) return candidate;
7687
+ if (await pathExists3(join22(candidate, "harness", "manifests"))) return candidate;
7443
7688
  }
7444
7689
  return null;
7445
7690
  }
7446
7691
  async function readCachedNpmPackageVersion(cacheRoot) {
7447
- const manifestPath = join21(cacheRoot, "package.json");
7692
+ const manifestPath = join22(cacheRoot, "package.json");
7448
7693
  if (!await pathExists3(manifestPath)) return null;
7449
7694
  try {
7450
- const pkg = JSON.parse(await readFile19(manifestPath, "utf8"));
7695
+ const pkg = JSON.parse(await readFile21(manifestPath, "utf8"));
7451
7696
  return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : null;
7452
7697
  } catch {
7453
7698
  return null;
@@ -7469,13 +7714,13 @@ async function latestWorkflowCacheIsStale(cacheRoot, packageName, resolveManifes
7469
7714
  }
7470
7715
  }
7471
7716
  async function materializeWorkflowPackageCache(cacheRoot, packageSpec, extract) {
7472
- await mkdir7(cacheRoot, { recursive: true });
7473
- const staging = join21(cacheRoot, ".extract");
7717
+ await mkdir8(cacheRoot, { recursive: true });
7718
+ const staging = join22(cacheRoot, ".extract");
7474
7719
  await rm8(staging, { recursive: true, force: true });
7475
- await mkdir7(staging, { recursive: true });
7720
+ await mkdir8(staging, { recursive: true });
7476
7721
  await extract(packageSpec, staging);
7477
- const packageDir = await pathExists3(join21(staging, "harness", "manifests")) ? staging : join21(staging, "package");
7478
- if (!await pathExists3(join21(packageDir, "harness", "manifests"))) {
7722
+ const packageDir = await pathExists3(join22(staging, "harness", "manifests")) ? staging : join22(staging, "package");
7723
+ if (!await pathExists3(join22(packageDir, "harness", "manifests"))) {
7479
7724
  throw new WorkflowDataResolutionError(
7480
7725
  "\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u5185\u5BB9\u65E0\u6548\uFF1A\u7F3A\u5C11 harness/manifests",
7481
7726
  "WORKFLOW_DATA_INVALID",
@@ -7504,8 +7749,8 @@ async function resolveWorkflowResourcesRoot(options, argv = []) {
7504
7749
  const packageName = workflowPackageName(family, options.env);
7505
7750
  const packageSpec = version === "latest" ? packageName : `${packageName}@${version}`;
7506
7751
  const cacheKey = packageSpec.replace("/", "+");
7507
- const cacheRoot = join21(options.cwd, ".harness", "cache", "workflow-packages", cacheKey);
7508
- if (await pathExists3(join21(cacheRoot, "harness", "manifests"))) {
7752
+ const cacheRoot = join22(options.cwd, ".harness", "cache", "workflow-packages", cacheKey);
7753
+ if (await pathExists3(join22(cacheRoot, "harness", "manifests"))) {
7509
7754
  if (version === "latest") {
7510
7755
  const stale = await latestWorkflowCacheIsStale(cacheRoot, packageName, options.pacoteManifest);
7511
7756
  if (stale) {
@@ -7551,9 +7796,9 @@ function describeWorkflowDataFetchFailure(error, packageSpec) {
7551
7796
  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}`;
7552
7797
  }
7553
7798
  async function readWorkflowFamilyManifest(resourcesRoot) {
7554
- const manifestPath = join21(resourcesRoot, "hunter-workflow-family.json");
7799
+ const manifestPath = join22(resourcesRoot, "hunter-workflow-family.json");
7555
7800
  if (!await pathExists3(manifestPath)) return {};
7556
- return JSON.parse(await readFile19(manifestPath, "utf8"));
7801
+ return JSON.parse(await readFile21(manifestPath, "utf8"));
7557
7802
  }
7558
7803
 
7559
7804
  // src/bin.ts
@@ -7673,6 +7918,12 @@ async function runCli(argv, overrides = {}) {
7673
7918
  dependencies
7674
7919
  );
7675
7920
  });
7921
+ program.command("rules-review").description("\u5BFC\u51FA\u5F85\u8BC4\u5BA1\u516C\u5171\u89C4\u5219\u5019\u9009\uFF0C\u6216\u5E94\u7528\u7ECF\u7528\u6237\u786E\u8BA4\u7684\u89C4\u5219\u51B3\u7B56").option("--apply <file>", "\u5E94\u7528\u5305\u542B\u5019\u9009 revision \u548C\u76EE\u6807 hash \u7684\u51B3\u7B56 JSON").option("--json").action(async (options) => {
7922
+ exitCode = await runRulesReview(
7923
+ { ...program.opts(), ...options },
7924
+ dependencies
7925
+ );
7926
+ });
7676
7927
  try {
7677
7928
  await program.parseAsync(["node", "hunter-harness", ...argv]);
7678
7929
  return exitCode;