hunter-harness 0.2.12 → 0.2.14

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 +961 -381
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -1115,7 +1115,7 @@ async function assertNoSymlinks(root, relativePath) {
1115
1115
  async function withRetry(action, options) {
1116
1116
  const attempts = options.attempts ?? 3;
1117
1117
  const sleep = options.sleep ?? (async (milliseconds) => {
1118
- await new Promise((resolve7) => setTimeout(resolve7, milliseconds));
1118
+ await new Promise((resolve8) => setTimeout(resolve8, milliseconds));
1119
1119
  });
1120
1120
  let lastError;
1121
1121
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
@@ -3457,8 +3457,8 @@ function resolvePushAuth(input) {
3457
3457
  }
3458
3458
 
3459
3459
  // ../core/dist/push/push.js
3460
- import { lstat as lstat2, readFile as readFile9, readdir as readdir4, rm as rm4 } from "node:fs/promises";
3461
- import { join as join10, relative, resolve as resolve5 } from "node:path";
3460
+ import { lstat as lstat3, readFile as readFile10, readdir as readdir4, rm as rm5 } from "node:fs/promises";
3461
+ import { join as join11, relative, resolve as resolve6 } from "node:path";
3462
3462
  import { parse as parseYaml5, stringify as stringifyYaml4 } from "yaml";
3463
3463
 
3464
3464
  // ../core/dist/state/baseline.js
@@ -3519,6 +3519,659 @@ async function acquireProtocolLock(projectRoot, operation, options = {}) {
3519
3519
  throw new Error("unable to acquire protocol lock");
3520
3520
  }
3521
3521
 
3522
+ // ../core/dist/sync/synchronize.js
3523
+ import { lstat as lstat2, readFile as readFile9, rm as rm4 } from "node:fs/promises";
3524
+ import { join as join10, resolve as resolve5 } from "node:path";
3525
+
3526
+ // ../core/dist/update/conflicts.js
3527
+ function operationTargetPath(operation) {
3528
+ return operation.operation === "rename" ? operation.to_path : operation.path;
3529
+ }
3530
+ function operationSourcePath(operation) {
3531
+ return operation.operation === "rename" ? operation.from_path : operation.path;
3532
+ }
3533
+ function operationAlreadyApplied(operation, baseline, projectVersion) {
3534
+ const target = baseline.files[operationTargetPath(operation)];
3535
+ if (operation.operation === "delete") {
3536
+ return target?.deleted === true && target.last_applied_version === projectVersion;
3537
+ }
3538
+ return target?.deleted === false && target?.last_applied_version === projectVersion && target.baseline_hash === operation.content_sha256;
3539
+ }
3540
+ function managedBlockDirty(currentContent, managedBlockHash) {
3541
+ if (managedBlockHash === void 0) {
3542
+ return true;
3543
+ }
3544
+ const block = extractManagedBlock(currentContent);
3545
+ return block === null || sha256Bytes(block) !== managedBlockHash;
3546
+ }
3547
+
3548
+ // ../core/dist/sync/artifact-rebase.js
3549
+ function expectedBase(operation) {
3550
+ return operation.operation === "add" ? null : operation.base_content_sha256;
3551
+ }
3552
+ function contentHash(operation) {
3553
+ return operation.operation === "delete" ? null : operation.content_sha256;
3554
+ }
3555
+ function baselineEntryFor(operation, finalContent, projectVersion) {
3556
+ const block = finalContent === null ? null : extractManagedBlock(finalContent);
3557
+ const remoteHash = contentHash(operation);
3558
+ const localHash = finalContent === null ? null : sha256Bytes(finalContent);
3559
+ return {
3560
+ baseline_hash: remoteHash,
3561
+ local_hash_at_apply: localHash,
3562
+ file_kind: operation.file_kind,
3563
+ last_applied_version: projectVersion,
3564
+ deleted: operation.operation === "delete",
3565
+ ...block === null ? {} : { managed_block_hash: sha256Bytes(block) }
3566
+ };
3567
+ }
3568
+ function acknowledgedEntry(operation, context, projectVersion, reason) {
3569
+ void reason;
3570
+ const localContent = operation.operation === "delete" ? context.sourceContent : context.targetContent ?? context.sourceContent;
3571
+ const block = localContent === null ? null : extractManagedBlock(localContent);
3572
+ return {
3573
+ baseline_hash: contentHash(operation),
3574
+ local_hash_at_apply: localContent === null ? null : sha256Bytes(localContent),
3575
+ file_kind: operation.file_kind,
3576
+ last_applied_version: projectVersion,
3577
+ deleted: operation.operation === "delete",
3578
+ ...block === null ? {} : { managed_block_hash: sha256Bytes(block) }
3579
+ };
3580
+ }
3581
+ function deletedSourceEntry(operation, projectVersion) {
3582
+ return {
3583
+ baseline_hash: null,
3584
+ local_hash_at_apply: null,
3585
+ file_kind: operation.file_kind,
3586
+ last_applied_version: projectVersion,
3587
+ deleted: true
3588
+ };
3589
+ }
3590
+ function isRenameTargetCollision(operation, targetContent, incomingHash) {
3591
+ if (operation.operation !== "rename" || targetContent === null) {
3592
+ return false;
3593
+ }
3594
+ return incomingHash === null || sha256Bytes(targetContent) !== incomingHash;
3595
+ }
3596
+ function resolveDecision(path, reason, operation, conflictStrategy, resolveOverrides) {
3597
+ if (reason === "target-collision") {
3598
+ return "conflict";
3599
+ }
3600
+ if (reason === "local-dirty" && operation.operation === "rename") {
3601
+ return "conflict";
3602
+ }
3603
+ const override = resolveOverrides?.get(path);
3604
+ if (override !== void 0) {
3605
+ return override;
3606
+ }
3607
+ if (conflictStrategy === "keep-local") {
3608
+ return "keep-local";
3609
+ }
3610
+ if (conflictStrategy === "accept-remote") {
3611
+ return "accept-remote";
3612
+ }
3613
+ return "conflict";
3614
+ }
3615
+ function computeFinalContent(operation, policy, targetContent, sourceContent, incoming) {
3616
+ const incomingHash = contentHash(operation);
3617
+ let equivalent = incomingHash !== null && targetContent !== null && sha256Bytes(targetContent) === incomingHash;
3618
+ let finalContent = incoming;
3619
+ if (policy.update_policy === "managed-block-only") {
3620
+ if (operation.operation === "delete") {
3621
+ finalContent = sourceContent === null ? null : removeManagedBlock(sourceContent);
3622
+ equivalent = finalContent === sourceContent;
3623
+ } else if (incoming !== null) {
3624
+ const incomingBlock = extractManagedBlock(incoming) ?? incoming.trim();
3625
+ const incomingId = extractSingleManagedBlockById(incoming)?.id;
3626
+ const blockId = operation.operation === "add" || operation.operation === "modify" ? operation.block_id ?? incomingId : void 0;
3627
+ finalContent = blockId !== void 0 ? upsertManagedBlockById(targetContent ?? "", blockId, incomingBlock) : upsertManagedBlock(targetContent ?? "", incomingBlock);
3628
+ equivalent = finalContent === targetContent;
3629
+ }
3630
+ } else if (operation.operation === "delete" && sourceContent === null) {
3631
+ equivalent = true;
3632
+ finalContent = null;
3633
+ } else if (equivalent) {
3634
+ finalContent = targetContent;
3635
+ }
3636
+ return { finalContent, equivalent };
3637
+ }
3638
+ function isDirty(operation, policy, previous, sourceContent, targetContent, incoming) {
3639
+ const incomingHash = contentHash(operation);
3640
+ let equivalent = incomingHash !== null && targetContent !== null && sha256Bytes(targetContent) === incomingHash;
3641
+ let dirty = false;
3642
+ if (operation.operation === "add") {
3643
+ if (targetContent !== null && !equivalent) {
3644
+ if (policy.update_policy === "managed-block-only" && incoming !== null) {
3645
+ const incomingBlock = extractManagedBlock(incoming) ?? incoming.trim();
3646
+ equivalent = extractManagedBlock(targetContent) === incomingBlock;
3647
+ dirty = !equivalent;
3648
+ } else {
3649
+ dirty = true;
3650
+ }
3651
+ }
3652
+ } else if (equivalent) {
3653
+ dirty = false;
3654
+ } else if (sourceContent === null) {
3655
+ dirty = operation.operation !== "delete";
3656
+ } else if (policy.update_policy === "managed-block-only") {
3657
+ dirty = previous?.managed_block_hash === void 0 ? (previous?.local_hash_at_apply ?? previous?.baseline_hash) !== sha256Bytes(sourceContent) : managedBlockDirty(sourceContent, previous.managed_block_hash);
3658
+ } else {
3659
+ dirty = (previous?.local_hash_at_apply ?? previous?.baseline_hash) !== sha256Bytes(sourceContent);
3660
+ }
3661
+ if (operation.operation === "rename" && targetContent !== null && !equivalent) {
3662
+ dirty = true;
3663
+ }
3664
+ return { dirty, equivalent };
3665
+ }
3666
+ function pushBaselineUpdates(plan, operation, target, entry) {
3667
+ plan.baselineUpdates.push({ path: target, entry });
3668
+ if (operation.operation === "rename") {
3669
+ plan.baselineUpdates.push({
3670
+ path: operation.from_path,
3671
+ entry: deletedSourceEntry(operation, entry.last_applied_version ?? null)
3672
+ });
3673
+ }
3674
+ }
3675
+ function recordWrite(plan, bucket, operation, target, content, equivalent, entry) {
3676
+ const write = { operation, path: target, content, equivalent };
3677
+ plan[bucket].push(write);
3678
+ pushBaselineUpdates(plan, operation, target, entry);
3679
+ }
3680
+ function planArtifactRebase(input) {
3681
+ const plan = {
3682
+ applied: [],
3683
+ acknowledged: [],
3684
+ resolvedKeepLocal: [],
3685
+ resolvedAcceptRemote: [],
3686
+ alreadyApplied: [],
3687
+ conflicts: [],
3688
+ baselineUpdates: [],
3689
+ baselineAdvanced: false
3690
+ };
3691
+ for (const context of input.contexts) {
3692
+ const operation = context.operation;
3693
+ const incoming = context.incomingContent;
3694
+ const source = operationSourcePath(operation);
3695
+ const target = operationTargetPath(operation);
3696
+ const policy = classifyFile(target);
3697
+ const incomingHash = contentHash(operation);
3698
+ if (operationAlreadyApplied(operation, input.baseline, input.projectVersion)) {
3699
+ plan.alreadyApplied.push({ path: target, operation });
3700
+ continue;
3701
+ }
3702
+ const staticDecision = decideUpdate(policy, false);
3703
+ if (!staticDecision.apply) {
3704
+ const acknowledgeReason = staticDecision.reason === "protocol-only" ? "protocol-only" : "policy-never";
3705
+ const entry2 = acknowledgedEntry(operation, context, input.projectVersion, acknowledgeReason);
3706
+ plan.acknowledged.push({
3707
+ operation,
3708
+ path: target,
3709
+ content: null,
3710
+ equivalent: true,
3711
+ acknowledgeReason
3712
+ });
3713
+ pushBaselineUpdates(plan, operation, target, entry2);
3714
+ continue;
3715
+ }
3716
+ const previous = input.baseline.files[source];
3717
+ const expected = expectedBase(operation);
3718
+ const actualBaseline = previous?.baseline_hash ?? null;
3719
+ if (expected !== actualBaseline) {
3720
+ const decision = resolveDecision(target, "baseline-diverged", operation, input.conflictStrategy, input.resolveOverrides);
3721
+ if (decision === "conflict") {
3722
+ const conflict2 = {
3723
+ path: target,
3724
+ operation: operation.operation,
3725
+ reason: "baseline-diverged",
3726
+ actualBaselineHash: actualBaseline
3727
+ };
3728
+ if (expected !== null) {
3729
+ conflict2.expectedBaselineHash = expected;
3730
+ }
3731
+ plan.conflicts.push(conflict2);
3732
+ continue;
3733
+ }
3734
+ if (decision === "keep-local") {
3735
+ const localContent = operation.operation === "rename" ? context.sourceContent : context.targetContent ?? context.sourceContent;
3736
+ const entry2 = baselineEntryFor(operation, localContent, input.projectVersion);
3737
+ recordWrite(plan, "resolvedKeepLocal", operation, target, localContent, true, entry2);
3738
+ continue;
3739
+ }
3740
+ if (decision === "accept-remote") {
3741
+ const { finalContent: finalContent2, equivalent: equivalent2 } = computeFinalContent(operation, policy, context.targetContent, context.sourceContent, incoming);
3742
+ const entry2 = baselineEntryFor(operation, finalContent2, input.projectVersion);
3743
+ recordWrite(plan, "resolvedAcceptRemote", operation, target, finalContent2, equivalent2, entry2);
3744
+ continue;
3745
+ }
3746
+ }
3747
+ const targetCollision = isRenameTargetCollision(operation, context.targetContent, incomingHash);
3748
+ if (targetCollision) {
3749
+ plan.conflicts.push({
3750
+ path: target,
3751
+ operation: operation.operation,
3752
+ reason: "target-collision"
3753
+ });
3754
+ continue;
3755
+ }
3756
+ const { dirty, equivalent: preEquivalent } = isDirty(operation, policy, previous, context.sourceContent, context.targetContent, incoming);
3757
+ if (dirty) {
3758
+ const reason = operation.operation === "rename" && context.targetContent !== null && !preEquivalent ? "target-collision" : "local-dirty";
3759
+ const decision = resolveDecision(target, reason, operation, input.conflictStrategy, input.resolveOverrides);
3760
+ if (decision === "conflict") {
3761
+ plan.conflicts.push({
3762
+ path: target,
3763
+ operation: operation.operation,
3764
+ reason
3765
+ });
3766
+ continue;
3767
+ }
3768
+ if (decision === "keep-local") {
3769
+ const localContent = operation.operation === "delete" ? context.sourceContent : operation.operation === "rename" ? context.sourceContent : context.targetContent ?? context.sourceContent;
3770
+ const entry3 = baselineEntryFor(operation, localContent, input.projectVersion);
3771
+ recordWrite(plan, "resolvedKeepLocal", operation, target, localContent, true, entry3);
3772
+ continue;
3773
+ }
3774
+ const { finalContent: finalContent2, equivalent: equivalent2 } = computeFinalContent(operation, policy, context.targetContent, context.sourceContent, incoming);
3775
+ const entry2 = baselineEntryFor(operation, finalContent2, input.projectVersion);
3776
+ recordWrite(plan, "resolvedAcceptRemote", operation, target, finalContent2, equivalent2, entry2);
3777
+ continue;
3778
+ }
3779
+ const { finalContent, equivalent } = computeFinalContent(operation, policy, context.targetContent, context.sourceContent, incoming);
3780
+ const entry = baselineEntryFor(operation, finalContent, input.projectVersion);
3781
+ if (equivalent) {
3782
+ plan.alreadyApplied.push({ path: target, operation });
3783
+ pushBaselineUpdates(plan, operation, target, entry);
3784
+ continue;
3785
+ }
3786
+ recordWrite(plan, "applied", operation, target, finalContent, equivalent, entry);
3787
+ }
3788
+ plan.baselineAdvanced = plan.conflicts.length === 0;
3789
+ return plan;
3790
+ }
3791
+
3792
+ // ../core/dist/sync/synchronize.js
3793
+ var MAX_SYNC_ARTIFACT_ITERATIONS = 50;
3794
+ async function pathExists(path) {
3795
+ try {
3796
+ await lstat2(path);
3797
+ return true;
3798
+ } catch (error) {
3799
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
3800
+ return false;
3801
+ }
3802
+ throw error;
3803
+ }
3804
+ }
3805
+ async function optionalContent(root, path) {
3806
+ const full = join10(root, path);
3807
+ return await pathExists(full) ? readFile9(full, "utf8") : null;
3808
+ }
3809
+ function manifestPayloadHash(manifest) {
3810
+ const payload = { ...manifest };
3811
+ delete payload.manifest_sha256;
3812
+ return sha256Bytes(canonicalJson(payload));
3813
+ }
3814
+ async function loadBlob(root, client, artifactId, operation, requestId, dryRun) {
3815
+ if (operation.operation === "delete") {
3816
+ return null;
3817
+ }
3818
+ const hash = operation.content_sha256;
3819
+ const cacheRoot = join10(root, ".harness", "cache", "server-artifacts", artifactId);
3820
+ const cachePath = join10(cacheRoot, "blobs", hash.replace(":", "_"));
3821
+ if (await pathExists(cachePath) && await sha256File(cachePath) === hash) {
3822
+ return readFile9(cachePath, "utf8");
3823
+ }
3824
+ const bytes = await client.downloadArtifactBlob(artifactId, hash, requestId);
3825
+ if (bytes.byteLength !== operation.size_bytes || sha256Bytes(bytes) !== hash) {
3826
+ await rm4(cachePath, { force: true });
3827
+ throw new Error("ARTIFACT_HASH_MISMATCH");
3828
+ }
3829
+ if (!dryRun) {
3830
+ await atomicWriteFile(cachePath, bytes);
3831
+ }
3832
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
3833
+ }
3834
+ function writeFromPlan(item2, keepLocalNoWrite) {
3835
+ if (keepLocalNoWrite || item2.equivalent) {
3836
+ return null;
3837
+ }
3838
+ const operation = item2.operation;
3839
+ const target = operationTargetPath(operation);
3840
+ if (operation.operation === "delete") {
3841
+ return { operation: "delete", path: target };
3842
+ }
3843
+ if (operation.operation === "rename") {
3844
+ return {
3845
+ operation: "rename",
3846
+ from_path: operation.from_path,
3847
+ to_path: operation.to_path,
3848
+ content: item2.content ?? ""
3849
+ };
3850
+ }
3851
+ return {
3852
+ operation: "modify",
3853
+ path: target,
3854
+ content: item2.content ?? ""
3855
+ };
3856
+ }
3857
+ function collectWrites(plan) {
3858
+ const ops = [];
3859
+ for (const item2 of plan.applied) {
3860
+ const op = writeFromPlan(item2, false);
3861
+ if (op !== null)
3862
+ ops.push(op);
3863
+ }
3864
+ for (const item2 of plan.resolvedAcceptRemote) {
3865
+ const op = writeFromPlan(item2, false);
3866
+ if (op !== null)
3867
+ ops.push(op);
3868
+ }
3869
+ for (const item2 of plan.resolvedKeepLocal) {
3870
+ const op = writeFromPlan(item2, true);
3871
+ if (op !== null)
3872
+ ops.push(op);
3873
+ }
3874
+ return ops;
3875
+ }
3876
+ function pathsFromPlan(plan) {
3877
+ const applied = plan.applied.map((item2) => item2.path);
3878
+ const acknowledged = plan.acknowledged.map((item2) => ({
3879
+ path: item2.path,
3880
+ operation: item2.operation.operation,
3881
+ reason: item2.acknowledgeReason ?? "policy-never"
3882
+ }));
3883
+ return {
3884
+ applied,
3885
+ acknowledged,
3886
+ resolvedKeepLocal: plan.resolvedKeepLocal.map((item2) => item2.path),
3887
+ resolvedAcceptRemote: plan.resolvedAcceptRemote.map((item2) => item2.path),
3888
+ alreadyApplied: plan.alreadyApplied.map((item2) => item2.path)
3889
+ };
3890
+ }
3891
+ async function buildContexts(root, manifest, client, requestId, dryRun) {
3892
+ const contexts = [];
3893
+ for (const operation of manifest.files) {
3894
+ const source = operationSourcePath(operation);
3895
+ const target = operationTargetPath(operation);
3896
+ const incoming = await loadBlob(root, client, manifest.artifact_id, operation, requestId, dryRun);
3897
+ contexts.push({
3898
+ operation,
3899
+ incomingContent: incoming,
3900
+ sourceContent: await optionalContent(root, source),
3901
+ targetContent: target === source ? await optionalContent(root, source) : await optionalContent(root, target)
3902
+ });
3903
+ }
3904
+ return contexts;
3905
+ }
3906
+ function applyBaselineUpdates(baseline, plan) {
3907
+ const next = baselineManifestSchema.parse(structuredClone(baseline));
3908
+ for (const update of plan.baselineUpdates) {
3909
+ next.files[update.path] = update.entry;
3910
+ }
3911
+ return next;
3912
+ }
3913
+ async function planSingleArtifact(root, baseline, manifest, client, requestId, dryRun, conflictStrategy, resolveOverrides) {
3914
+ if (manifest.project_version === null) {
3915
+ throw new Error("artifact manifest missing project_version");
3916
+ }
3917
+ const contexts = await buildContexts(root, manifest, client, requestId, dryRun);
3918
+ return planArtifactRebase({
3919
+ baseline,
3920
+ projectVersion: manifest.project_version,
3921
+ contexts,
3922
+ conflictStrategy,
3923
+ ...resolveOverrides === void 0 ? {} : { resolveOverrides }
3924
+ });
3925
+ }
3926
+ async function saveConflictReport(root, requestId, manifest, plan) {
3927
+ const reportPath = join10(root, ".harness", "reports", "conflicts-" + requestId + ".json");
3928
+ await atomicWriteJson(reportPath, {
3929
+ schema_version: 1,
3930
+ request_id: requestId,
3931
+ artifact_id: manifest.artifact_id,
3932
+ project_version: manifest.project_version,
3933
+ conflicts: plan.conflicts,
3934
+ resolve_hint: "npx hunter-harness update --resolve <path>=keep-local|accept-remote"
3935
+ });
3936
+ }
3937
+ async function synchronizeArtifacts(options, initialBaseline) {
3938
+ const root = resolve5(options.projectRoot);
3939
+ const projectId = options.project.project.project_id;
3940
+ if (projectId === null) {
3941
+ throw new Error("PROJECT_NOT_BOUND");
3942
+ }
3943
+ let baseline = initialBaseline;
3944
+ const seenArtifactIds = /* @__PURE__ */ new Set();
3945
+ const aggregate = {
3946
+ requestId: options.requestId,
3947
+ projectId,
3948
+ artifactsProcessed: 0,
3949
+ applied: [],
3950
+ acknowledged: [],
3951
+ resolvedKeepLocal: [],
3952
+ resolvedAcceptRemote: [],
3953
+ alreadyApplied: [],
3954
+ conflicts: [],
3955
+ skipped: [],
3956
+ operations: [],
3957
+ artifactId: null,
3958
+ observedProjectVersion: null,
3959
+ transactionId: null,
3960
+ dryRun: options.dryRun,
3961
+ baselineAdvanced: false
3962
+ };
3963
+ const conflictStrategy = options.conflictStrategy ?? "manual";
3964
+ for (let iteration = 0; iteration < MAX_SYNC_ARTIFACT_ITERATIONS; iteration++) {
3965
+ const discovery = await options.client.getUpdateManifest(projectId, {
3966
+ base_project_version: baseline.complete_project_version,
3967
+ base_manifest_hash: sha256Bytes(canonicalJson(baseline)),
3968
+ adapter: options.project.adapters.enabled[0] ?? "claude-code",
3969
+ profile: options.project.project.profiles[0] ?? "general"
3970
+ }, options.requestId);
3971
+ aggregate.observedProjectVersion = discovery.observed_project_version;
3972
+ if (!discovery.delta_available || discovery.artifact_id === null) {
3973
+ aggregate.baselineAdvanced = aggregate.conflicts.length === 0;
3974
+ return aggregate;
3975
+ }
3976
+ if (seenArtifactIds.has(discovery.artifact_id)) {
3977
+ throw new Error("DUPLICATE_ARTIFACT_ID:" + discovery.artifact_id);
3978
+ }
3979
+ seenArtifactIds.add(discovery.artifact_id);
3980
+ if (options.stopAfterArtifactId === discovery.artifact_id) {
3981
+ return aggregate;
3982
+ }
3983
+ const manifest = artifactManifestSchema.parse(await options.client.getArtifactManifest(discovery.artifact_id, options.requestId));
3984
+ if (manifest.artifact_id !== discovery.artifact_id || manifest.project_id !== projectId || manifest.project_version === null || manifestPayloadHash(manifest) !== manifest.manifest_sha256) {
3985
+ throw new Error("ARTIFACT_HASH_MISMATCH");
3986
+ }
3987
+ let plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, conflictStrategy, options.resolveOverrides);
3988
+ if (plan.conflicts.length > 0 && options.confirmConflictStrategy !== void 0) {
3989
+ const confirmed = await options.confirmConflictStrategy(plan.conflicts);
3990
+ if (confirmed === false) {
3991
+ aggregate.conflicts = plan.conflicts;
3992
+ aggregate.skipped = plan.conflicts.map((item2) => ({
3993
+ path: item2.path,
3994
+ operation: item2.operation,
3995
+ reason: item2.reason
3996
+ }));
3997
+ aggregate.operations = manifest.files;
3998
+ aggregate.artifactId = manifest.artifact_id;
3999
+ return aggregate;
4000
+ }
4001
+ plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, confirmed, options.resolveOverrides);
4002
+ }
4003
+ const paths = pathsFromPlan(plan);
4004
+ aggregate.applied.push(...paths.applied);
4005
+ aggregate.acknowledged.push(...paths.acknowledged);
4006
+ aggregate.resolvedKeepLocal.push(...paths.resolvedKeepLocal);
4007
+ aggregate.resolvedAcceptRemote.push(...paths.resolvedAcceptRemote);
4008
+ aggregate.alreadyApplied.push(...paths.alreadyApplied);
4009
+ aggregate.operations = manifest.files;
4010
+ aggregate.artifactId = manifest.artifact_id;
4011
+ aggregate.artifactsProcessed += 1;
4012
+ if (plan.conflicts.length > 0) {
4013
+ aggregate.conflicts = plan.conflicts;
4014
+ aggregate.skipped = plan.conflicts.map((item2) => ({
4015
+ path: item2.path,
4016
+ operation: item2.operation,
4017
+ reason: item2.reason
4018
+ }));
4019
+ if (conflictStrategy !== "manual") {
4020
+ if (!options.dryRun) {
4021
+ await saveConflictReport(root, options.requestId, manifest, plan);
4022
+ }
4023
+ return aggregate;
4024
+ }
4025
+ }
4026
+ const hasApplicableWork = plan.applied.length > 0 || plan.acknowledged.length > 0 || plan.resolvedKeepLocal.length > 0 || plan.resolvedAcceptRemote.length > 0 || plan.baselineUpdates.length > 0;
4027
+ if (plan.conflicts.length > 0 && !hasApplicableWork) {
4028
+ if (!options.dryRun) {
4029
+ await saveConflictReport(root, options.requestId, manifest, plan);
4030
+ }
4031
+ return aggregate;
4032
+ }
4033
+ if (options.dryRun) {
4034
+ baseline = applyBaselineUpdates(baseline, plan);
4035
+ if (plan.baselineAdvanced && plan.conflicts.length === 0) {
4036
+ baseline.complete_project_version = manifest.project_version;
4037
+ baseline.artifact_manifest_hash = manifest.manifest_sha256;
4038
+ baseline.latest_artifact_id = manifest.artifact_id;
4039
+ }
4040
+ if (plan.conflicts.length > 0) {
4041
+ return aggregate;
4042
+ }
4043
+ continue;
4044
+ }
4045
+ await atomicWriteJson(join10(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
4046
+ const lock = await acquireProtocolLock(root, "update", { requestId: options.requestId });
4047
+ try {
4048
+ const nextBaseline = applyBaselineUpdates(baseline, plan);
4049
+ if (plan.baselineAdvanced && plan.conflicts.length === 0) {
4050
+ nextBaseline.complete_project_version = manifest.project_version;
4051
+ nextBaseline.artifact_manifest_hash = manifest.manifest_sha256;
4052
+ nextBaseline.latest_artifact_id = manifest.artifact_id;
4053
+ }
4054
+ const transactionId = "tx_sync_" + Date.now() + "_" + options.requestId;
4055
+ const reportPath = ".harness/reports/update-" + options.requestId + ".json";
4056
+ const report = {
4057
+ schema_version: 2,
4058
+ request_id: options.requestId,
4059
+ artifact_id: manifest.artifact_id,
4060
+ observed_project_version: manifest.project_version,
4061
+ status: plan.conflicts.length === 0 ? "applied" : "partial_due_to_conflicts",
4062
+ applied: paths.applied,
4063
+ acknowledged: paths.acknowledged,
4064
+ resolved_keep_local: paths.resolvedKeepLocal,
4065
+ resolved_accept_remote: paths.resolvedAcceptRemote,
4066
+ already_applied: paths.alreadyApplied,
4067
+ conflicts: plan.conflicts,
4068
+ skipped: aggregate.skipped,
4069
+ transaction_id: transactionId
4070
+ };
4071
+ const fileOps = collectWrites(plan);
4072
+ fileOps.push({
4073
+ operation: "modify",
4074
+ path: ".harness/state/baseline/manifest.json",
4075
+ content: JSON.stringify(nextBaseline, null, 2) + "\n"
4076
+ }, {
4077
+ operation: "add",
4078
+ path: reportPath,
4079
+ content: JSON.stringify(report, null, 2) + "\n"
4080
+ }, {
4081
+ operation: "modify",
4082
+ path: ".harness/state/local/last-update.json",
4083
+ content: JSON.stringify(report, null, 2) + "\n"
4084
+ });
4085
+ await runTransaction(root, fileOps, {
4086
+ id: transactionId,
4087
+ kind: "update",
4088
+ ...options.transactionOptions ?? {}
4089
+ });
4090
+ aggregate.transactionId = transactionId;
4091
+ baseline = nextBaseline;
4092
+ if (plan.conflicts.length > 0) {
4093
+ await saveConflictReport(root, options.requestId, manifest, plan);
4094
+ aggregate.baselineAdvanced = false;
4095
+ return aggregate;
4096
+ }
4097
+ } finally {
4098
+ await lock.release();
4099
+ }
4100
+ }
4101
+ throw new Error("MAX_SYNC_ARTIFACT_ITERATIONS_EXCEEDED");
4102
+ }
4103
+ function baselineEntryFromOperation(manifest, operation, localContent) {
4104
+ const block = localContent === null ? null : extractManagedBlock(localContent);
4105
+ return {
4106
+ baseline_hash: operation.operation === "delete" ? null : operation.content_sha256,
4107
+ local_hash_at_apply: localContent === null ? null : sha256Bytes(localContent),
4108
+ file_kind: operation.file_kind,
4109
+ last_applied_version: manifest.project_version,
4110
+ deleted: operation.operation === "delete",
4111
+ ...block === null ? {} : { managed_block_hash: sha256Bytes(block) }
4112
+ };
4113
+ }
4114
+ async function advanceBaselineFromArtifact(options, baseline) {
4115
+ const root = resolve5(options.projectRoot);
4116
+ if (options.manifest.project_version === null) {
4117
+ throw new Error("artifact manifest missing project_version");
4118
+ }
4119
+ let localChanged = false;
4120
+ for (const operation of options.manifest.files) {
4121
+ const source = operationSourcePath(operation);
4122
+ const target = operationTargetPath(operation);
4123
+ const sourceContent = await optionalContent(root, source);
4124
+ const targetContent = target === source ? sourceContent : await optionalContent(root, target);
4125
+ if (operation.operation === "delete") {
4126
+ if (sourceContent !== null) {
4127
+ localChanged = true;
4128
+ }
4129
+ continue;
4130
+ }
4131
+ const expectedHash = operation.content_sha256;
4132
+ const actualContent = targetContent ?? sourceContent;
4133
+ const actualHash = actualContent === null ? null : sha256Bytes(actualContent);
4134
+ if (actualHash !== expectedHash) {
4135
+ localChanged = true;
4136
+ }
4137
+ }
4138
+ if (localChanged) {
4139
+ return { baseline, localChanged: true, transactionId: null };
4140
+ }
4141
+ const nextBaseline = baselineManifestSchema.parse(structuredClone(baseline));
4142
+ for (const operation of options.manifest.files) {
4143
+ const source = operationSourcePath(operation);
4144
+ const target = operationTargetPath(operation);
4145
+ const sourceContent = await optionalContent(root, source);
4146
+ const targetContent = target === source ? sourceContent : await optionalContent(root, target);
4147
+ const localContent = operation.operation === "delete" ? null : targetContent ?? sourceContent;
4148
+ nextBaseline.files[target] = baselineEntryFromOperation(options.manifest, operation, localContent);
4149
+ if (operation.operation === "rename") {
4150
+ nextBaseline.files[operation.from_path] = {
4151
+ baseline_hash: null,
4152
+ local_hash_at_apply: null,
4153
+ file_kind: operation.file_kind,
4154
+ last_applied_version: options.manifest.project_version,
4155
+ deleted: true
4156
+ };
4157
+ }
4158
+ }
4159
+ nextBaseline.complete_project_version = options.manifest.project_version;
4160
+ nextBaseline.artifact_manifest_hash = options.manifest.manifest_sha256;
4161
+ nextBaseline.latest_artifact_id = options.manifest.artifact_id;
4162
+ const transactionId = "tx_push_baseline_" + Date.now();
4163
+ await runTransaction(root, [{
4164
+ operation: "modify",
4165
+ path: ".harness/state/baseline/manifest.json",
4166
+ content: JSON.stringify(nextBaseline, null, 2) + "\n"
4167
+ }], {
4168
+ id: transactionId,
4169
+ kind: "update",
4170
+ ...options.transactionOptions ?? {}
4171
+ });
4172
+ return { baseline: nextBaseline, localChanged: false, transactionId };
4173
+ }
4174
+
3522
4175
  // ../core/dist/push/push.js
3523
4176
  var PushWorkflowError = class extends Error {
3524
4177
  exitCode;
@@ -3545,7 +4198,7 @@ var SHARED_MANAGED_FILES = [
3545
4198
  ];
3546
4199
  async function exists3(path) {
3547
4200
  try {
3548
- await lstat2(path);
4201
+ await lstat3(path);
3549
4202
  return true;
3550
4203
  } catch (error) {
3551
4204
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -3559,7 +4212,7 @@ async function walkFiles(root, current, output) {
3559
4212
  return;
3560
4213
  }
3561
4214
  for (const item2 of await readdir4(current, { withFileTypes: true })) {
3562
- const path = join10(current, item2.name);
4215
+ const path = join11(current, item2.name);
3563
4216
  if (item2.isSymbolicLink()) {
3564
4217
  throw new PushWorkflowError("symlink is not pushable", 6, "UNSAFE_SYMLINK");
3565
4218
  }
@@ -3581,7 +4234,7 @@ async function walkHarnessEntries(root, directory, output) {
3581
4234
  return;
3582
4235
  for (const item2 of await readdir4(directory, { withFileTypes: true })) {
3583
4236
  if (item2.name.startsWith("harness-")) {
3584
- const path = join10(directory, item2.name);
4237
+ const path = join11(directory, item2.name);
3585
4238
  if (item2.isDirectory()) {
3586
4239
  await walkFiles(root, path, output);
3587
4240
  } else if (item2.isFile()) {
@@ -3591,7 +4244,7 @@ async function walkHarnessEntries(root, directory, output) {
3591
4244
  }
3592
4245
  }
3593
4246
  async function managedFiles(projectRoot, project) {
3594
- const root = resolve5(projectRoot);
4247
+ const root = resolve6(projectRoot);
3595
4248
  const paths = [];
3596
4249
  const adapters = getAdapters(enabledHarnessAgents(project));
3597
4250
  const managedFiles2 = [
@@ -3600,25 +4253,25 @@ async function managedFiles(projectRoot, project) {
3600
4253
  ...adapters.some((adapter) => adapter.name === "codebuddy") ? ["CODEBUDDY.md"] : []
3601
4254
  ];
3602
4255
  for (const path of managedFiles2) {
3603
- if (await exists3(join10(root, path))) {
4256
+ if (await exists3(join11(root, path))) {
3604
4257
  paths.push(path);
3605
4258
  }
3606
4259
  }
3607
4260
  for (const path of SHARED_MANAGED_ROOTS) {
3608
- await walkFiles(root, join10(root, path), paths);
4261
+ await walkFiles(root, join11(root, path), paths);
3609
4262
  }
3610
4263
  for (const adapter of adapters) {
3611
4264
  if (adapter.rulesRoot !== null) {
3612
- await walkFiles(root, join10(root, adapter.rulesRoot), paths);
4265
+ await walkFiles(root, join11(root, adapter.rulesRoot), paths);
3613
4266
  }
3614
- await walkHarnessEntries(root, join10(root, adapter.skillsRoot), paths);
4267
+ await walkHarnessEntries(root, join11(root, adapter.skillsRoot), paths);
3615
4268
  if (adapter.agentsRoot !== null) {
3616
- await walkHarnessEntries(root, join10(root, adapter.agentsRoot), paths);
4269
+ await walkHarnessEntries(root, join11(root, adapter.agentsRoot), paths);
3617
4270
  }
3618
4271
  }
3619
4272
  const result = {};
3620
4273
  for (const path of [...new Set(paths)].sort()) {
3621
- result[path] = await readFile9(join10(root, path), "utf8");
4274
+ result[path] = await readFile10(join11(root, path), "utf8");
3622
4275
  }
3623
4276
  return result;
3624
4277
  }
@@ -3627,14 +4280,14 @@ function proposalBaseline(baseline) {
3627
4280
  }
3628
4281
  async function readProject(root) {
3629
4282
  try {
3630
- return projectConfigSchema.parse(parseYaml5(await readFile9(join10(root, ".harness", "project.yaml"), "utf8")));
4283
+ return projectConfigSchema.parse(parseYaml5(await readFile10(join11(root, ".harness", "project.yaml"), "utf8")));
3631
4284
  } catch {
3632
4285
  throw new PushWorkflowError("project configuration is missing or invalid", 3, "PROJECT_CONFIG_INVALID");
3633
4286
  }
3634
4287
  }
3635
4288
  async function readOptionalJson(path) {
3636
4289
  try {
3637
- return JSON.parse(await readFile9(path, "utf8"));
4290
+ return JSON.parse(await readFile10(path, "utf8"));
3638
4291
  } catch (error) {
3639
4292
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
3640
4293
  return null;
@@ -3646,7 +4299,7 @@ async function clientIdFor(root, explicit) {
3646
4299
  if (explicit !== void 0) {
3647
4300
  return explicit;
3648
4301
  }
3649
- const path = join10(root, ".harness", "state", "local", "client.json");
4302
+ const path = join11(root, ".harness", "state", "local", "client.json");
3650
4303
  const existing = await readOptionalJson(path);
3651
4304
  if (typeof existing?.client_id === "string" && /^cli_[A-Za-z0-9_-]+$/.test(existing.client_id)) {
3652
4305
  return existing.client_id;
@@ -3735,9 +4388,74 @@ function assertPreviewAllowed(preview, skip) {
3735
4388
  }
3736
4389
  }
3737
4390
  var CREDENTIALS_HINT = "\u53EF\u5728\u4EA4\u4E92\u6A21\u5F0F\u4E0B\u5F55\u5165\uFF0C\u6216\u5199\u5165 .harness/credentials.local.yaml\uFF08\u52FF\u63D0\u4EA4 git\uFF09";
3738
- var STALE_BASELINE_MESSAGE = "\u670D\u52A1\u7AEF\u5DF2\u6709\u66F4\u65B0\u7684\u63A8\u9001\uFF0C\u8BF7\u5148 git pull \u540E\u6267\u884C npx hunter-harness update \u518D\u63A8";
3739
- function staleBaselineError(code) {
3740
- return new PushWorkflowError(STALE_BASELINE_MESSAGE, 5, code);
4391
+ var CONFLICT_PREVIEW_LIMIT = 8;
4392
+ function conflictGroup(path) {
4393
+ const knowledgeMatch = path.match(/^\.harness\/knowledge\/entries\/([^/]+)\//);
4394
+ if (knowledgeMatch?.[1] !== void 0) {
4395
+ return `knowledge/${knowledgeMatch[1]}`;
4396
+ }
4397
+ if (path.startsWith(".harness/knowledge/"))
4398
+ return "knowledge/other";
4399
+ if (path === ".harness/context-index.json")
4400
+ return "context-index";
4401
+ return path.split("/", 1)[0] ?? "other";
4402
+ }
4403
+ function conflictGroups(conflicts) {
4404
+ const groups = {};
4405
+ for (const conflict2 of conflicts) {
4406
+ const group = conflictGroup(conflict2.path);
4407
+ groups[group] = (groups[group] ?? 0) + 1;
4408
+ }
4409
+ return groups;
4410
+ }
4411
+ function formatStaleBaselineMessage(conflicts = []) {
4412
+ const lines = [
4413
+ "\u670D\u52A1\u7AEF\u5B58\u5728\u672C\u5730\u5C1A\u672A\u786E\u8BA4\u7684\u66F4\u65B0\uFF0Cpush \u5DF2\u6682\u505C\u4EE5\u907F\u514D\u8986\u76D6\u5E76\u884C\u4FEE\u6539\u3002"
4414
+ ];
4415
+ if (conflicts.length > 0) {
4416
+ lines.push(`\u68C0\u6D4B\u5230 ${conflicts.length} \u4E2A\u51B2\u7A81\uFF08\u4EC5\u5C55\u793A\u524D ${CONFLICT_PREVIEW_LIMIT} \u4E2A\uFF09\uFF1A`);
4417
+ for (const conflict2 of conflicts.slice(0, CONFLICT_PREVIEW_LIMIT)) {
4418
+ lines.push(` - ${conflict2.path}`);
4419
+ }
4420
+ if (conflicts.length > CONFLICT_PREVIEW_LIMIT) {
4421
+ lines.push(` - \u2026\u53E6 ${conflicts.length - CONFLICT_PREVIEW_LIMIT} \u4E2A`);
4422
+ }
4423
+ }
4424
+ lines.push("\u4FDD\u7559\u672C\u5730\u5E76\u7EE7\u7EED\uFF1Anpx hunter-harness update --conflict-strategy keep-local --yes", "\u63A5\u53D7\u670D\u52A1\u7AEF\u7248\u672C\uFF1Anpx hunter-harness update --conflict-strategy accept-remote --yes", "\u9010\u9879\u5904\u7406\uFF1Anpx hunter-harness update --resolve <path>=keep-local|accept-remote");
4425
+ return lines.join("\n");
4426
+ }
4427
+ function staleBaselineError(code, conflicts) {
4428
+ const conflictList = [...conflicts ?? []];
4429
+ return new PushWorkflowError(formatStaleBaselineMessage(conflictList), 5, code, conflictList.length === 0 ? void 0 : {
4430
+ conflicts: conflictList,
4431
+ conflict_count: conflictList.length,
4432
+ conflict_groups: conflictGroups(conflictList)
4433
+ });
4434
+ }
4435
+ async function syncToLatest(root, project, baseline, client, confirmConflictStrategy) {
4436
+ const syncResult = await synchronizeArtifacts({
4437
+ projectRoot: root,
4438
+ project,
4439
+ client,
4440
+ requestId: uuidV7(),
4441
+ dryRun: false,
4442
+ conflictStrategy: "manual",
4443
+ ...confirmConflictStrategy === void 0 ? {} : { confirmConflictStrategy }
4444
+ }, baseline);
4445
+ if (syncResult.conflicts.length > 0) {
4446
+ throw staleBaselineError("PROJECT_VERSION_CONFLICT", syncResult.conflicts);
4447
+ }
4448
+ return await readBaseline(root);
4449
+ }
4450
+ async function autoRebaseIfServerAdvanced(root, project, baseline, client, remoteVersion, confirmConflictStrategy) {
4451
+ if (remoteVersion === baseline.complete_project_version) {
4452
+ return baseline;
4453
+ }
4454
+ const updated = await syncToLatest(root, project, baseline, client, confirmConflictStrategy);
4455
+ if (remoteVersion !== null && updated.complete_project_version !== remoteVersion) {
4456
+ throw staleBaselineError("PROJECT_VERSION_CONFLICT");
4457
+ }
4458
+ return updated;
3741
4459
  }
3742
4460
  function makePreview(baseline, files, confirmedProjectLocal, ignorePaths, deletedAt = (/* @__PURE__ */ new Date()).toISOString()) {
3743
4461
  const filteredFiles = {};
@@ -3777,7 +4495,7 @@ async function bindProject(root, project, baseline, projectId) {
3777
4495
  return { project: nextProject, baseline: nextBaseline };
3778
4496
  }
3779
4497
  async function pushProject(options) {
3780
- const root = resolve5(options.projectRoot);
4498
+ const root = resolve6(options.projectRoot);
3781
4499
  let project = await readProject(root);
3782
4500
  let baseline = await readBaseline(root);
3783
4501
  const profile = parseHarnessProfile(project.project.profiles[0]);
@@ -3830,9 +4548,8 @@ async function pushProject(options) {
3830
4548
  const boundAtStart = project.project.project_id;
3831
4549
  if (boundAtStart !== null) {
3832
4550
  const remote = await client.getProject(boundAtStart, uuidV7());
3833
- if (remote.latest_project_version !== baseline.complete_project_version) {
3834
- throw staleBaselineError("PROJECT_VERSION_CONFLICT");
3835
- }
4551
+ baseline = await autoRebaseIfServerAdvanced(root, project, baseline, client, remote.latest_project_version, options.confirmConflictStrategy);
4552
+ preview = makePreview(baseline, await managedFiles(root, project), options.confirmedProjectLocal ?? [], installedPaths);
3836
4553
  }
3837
4554
  const initialSkip = await resolveSensitiveScanSkip(preview, options);
3838
4555
  if (initialSkip.cancelled === true) {
@@ -3844,7 +4561,7 @@ async function pushProject(options) {
3844
4561
  if (options.confirmProposal !== void 0 && !await options.confirmProposal(preview)) {
3845
4562
  return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
3846
4563
  }
3847
- const workflowPath = join10(root, ".harness", "state", "local", "push-workflow.json");
4564
+ const workflowPath = join11(root, ".harness", "state", "local", "push-workflow.json");
3848
4565
  const priorWorkflow = await readOptionalJson(workflowPath);
3849
4566
  const provisionalRequestId = priorWorkflow?.local_project_key === project.project.local_project_key ? priorWorkflow.request_id : uuidV7();
3850
4567
  const lock = await acquireProtocolLock(root, "push", { requestId: provisionalRequestId });
@@ -3935,7 +4652,9 @@ async function pushProject(options) {
3935
4652
  });
3936
4653
  }
3937
4654
  }
3938
- const finalized = await client.finalizeProposal(session.session_id, {
4655
+ let finalizeRetried = false;
4656
+ let finalized;
4657
+ const finalizeOnce = async () => client.finalizeProposal(session.session_id, {
3939
4658
  schema_version: 1,
3940
4659
  manifest_sha256: proposalManifestHash,
3941
4660
  base_artifact_id: baseline.latest_artifact_id ?? null,
@@ -3944,7 +4663,60 @@ async function pushProject(options) {
3944
4663
  ...sensitiveScanSkipReason === void 0 ? {} : { sensitive_scan_skip_reason: sensitiveScanSkipReason }
3945
4664
  } : {}
3946
4665
  }, requestId, workflow.finalize_idempotency_key);
3947
- await atomicWriteJson(join10(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
4666
+ try {
4667
+ finalized = await finalizeOnce();
4668
+ } catch (error) {
4669
+ if (error instanceof ApiError && (error.code === "STALE_PUSH" || error.code === "PROJECT_VERSION_CONFLICT") && !finalizeRetried) {
4670
+ finalizeRetried = true;
4671
+ baseline = await syncToLatest(root, project, baseline, client, options.confirmConflictStrategy);
4672
+ workflow = resetSession(workflow, proposalManifestHash);
4673
+ await atomicWriteJson(workflowPath, workflow);
4674
+ session = await client.createProposalSession(projectId, {
4675
+ schema_version: 1,
4676
+ request_id: requestId,
4677
+ client_id: clientId,
4678
+ base_project_version: baseline.complete_project_version,
4679
+ base_manifest_hash: sha256Bytes(canonicalJson(baseline)),
4680
+ proposal_manifest: { files: preview.operations },
4681
+ artifact_manifest: { schema_version: 1, files: preview.operations }
4682
+ }, requestId, workflow.session_idempotency_key);
4683
+ workflow.session_id = session.session_id;
4684
+ workflow.session_expires_at = session.expires_at;
4685
+ workflow.max_chunk_bytes = session.max_chunk_bytes;
4686
+ await atomicWriteJson(workflowPath, workflow);
4687
+ try {
4688
+ finalized = await finalizeOnce();
4689
+ } catch (retryError) {
4690
+ if (retryError instanceof ApiError && (retryError.code === "STALE_PUSH" || retryError.code === "PROJECT_VERSION_CONFLICT")) {
4691
+ throw staleBaselineError(retryError.code);
4692
+ }
4693
+ throw retryError;
4694
+ }
4695
+ } else if (error instanceof ApiError && (error.code === "STALE_PUSH" || error.code === "PROJECT_VERSION_CONFLICT")) {
4696
+ throw staleBaselineError(error.code);
4697
+ } else {
4698
+ throw error;
4699
+ }
4700
+ }
4701
+ let pushWarning;
4702
+ if (finalized.artifact_id !== null) {
4703
+ try {
4704
+ const publishedManifest = artifactManifestSchema.parse(await client.getArtifactManifest(finalized.artifact_id, requestId));
4705
+ const advanced = await advanceBaselineFromArtifact({
4706
+ projectRoot: root,
4707
+ manifest: publishedManifest,
4708
+ requestId
4709
+ }, baseline);
4710
+ if (advanced.localChanged) {
4711
+ pushWarning = "LOCAL_CHANGED_DURING_PUSH";
4712
+ } else {
4713
+ baseline = advanced.baseline;
4714
+ }
4715
+ } catch {
4716
+ pushWarning = "BASELINE_ADVANCE_DEFERRED";
4717
+ }
4718
+ }
4719
+ await atomicWriteJson(join11(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
3948
4720
  schema_version: 1,
3949
4721
  request_id: requestId,
3950
4722
  project_id: projectId,
@@ -3952,10 +4724,17 @@ async function pushProject(options) {
3952
4724
  status: finalized.status,
3953
4725
  artifact_id: finalized.artifact_id,
3954
4726
  operation_count: preview.operations.length,
4727
+ warning: pushWarning ?? null,
3955
4728
  recorded_at: (/* @__PURE__ */ new Date()).toISOString()
3956
4729
  });
3957
- await rm4(workflowPath, { force: true });
3958
- return { preview, proposalId: finalized.proposal_id, projectId, artifactId: finalized.artifact_id };
4730
+ await rm5(workflowPath, { force: true });
4731
+ return {
4732
+ preview,
4733
+ proposalId: finalized.proposal_id,
4734
+ projectId,
4735
+ artifactId: finalized.artifact_id,
4736
+ ...pushWarning === void 0 ? {} : { warning: pushWarning }
4737
+ };
3959
4738
  } catch (error) {
3960
4739
  if (error instanceof PushWorkflowError) {
3961
4740
  throw error;
@@ -3982,8 +4761,8 @@ async function pushProject(options) {
3982
4761
  }
3983
4762
 
3984
4763
  // ../core/dist/state/cleanup.js
3985
- import { readFile as readFile10, readdir as readdir5, rm as rm5 } from "node:fs/promises";
3986
- import { join as join11 } from "node:path";
4764
+ import { readFile as readFile11, readdir as readdir5, rm as rm6 } from "node:fs/promises";
4765
+ import { join as join12 } from "node:path";
3987
4766
  function isSafeEntryName(name) {
3988
4767
  return name.length > 0 && !name.includes("/") && !name.includes("\\") && name !== "." && name !== ".." && !name.includes("\0");
3989
4768
  }
@@ -4007,7 +4786,7 @@ async function cleanupProject(options) {
4007
4786
  continue;
4008
4787
  let journal;
4009
4788
  try {
4010
- journal = JSON.parse(await readFile10(join11(layout.transactions, name, "journal.json"), "utf8"));
4789
+ journal = JSON.parse(await readFile11(join12(layout.transactions, name, "journal.json"), "utf8"));
4011
4790
  } catch {
4012
4791
  continue;
4013
4792
  }
@@ -4025,7 +4804,7 @@ async function cleanupProject(options) {
4025
4804
  continue;
4026
4805
  pruned.push(entry.id);
4027
4806
  if (!options.dryRun) {
4028
- await rm5(join11(layout.transactions, entry.id), { recursive: true, force: true });
4807
+ await rm6(join12(layout.transactions, entry.id), { recursive: true, force: true });
4029
4808
  }
4030
4809
  }
4031
4810
  }
@@ -4034,7 +4813,7 @@ async function cleanupProject(options) {
4034
4813
  continue;
4035
4814
  removedCache.push(name);
4036
4815
  if (!options.dryRun) {
4037
- await rm5(join11(layout.serverArtifacts, name), { recursive: true, force: true });
4816
+ await rm6(join12(layout.serverArtifacts, name), { recursive: true, force: true });
4038
4817
  }
4039
4818
  }
4040
4819
  return {
@@ -4088,10 +4867,10 @@ var AGENT_DESCRIPTORS = {
4088
4867
  var INSTALLABLE_AGENTS = Object.keys(AGENT_DESCRIPTORS).filter((agent) => AGENT_DESCRIPTORS[agent]?.installable === true);
4089
4868
 
4090
4869
  // ../core/dist/transaction/recovery.js
4091
- import { readFile as readFile11, readdir as readdir6, rm as rm6, stat as stat3 } from "node:fs/promises";
4092
- import { join as join12 } from "node:path";
4870
+ import { readFile as readFile12, readdir as readdir6, rm as rm7, stat as stat3 } from "node:fs/promises";
4871
+ import { join as join13 } from "node:path";
4093
4872
  async function recoverTransaction(projectRoot, transactionId) {
4094
- const journal = JSON.parse(await readFile11(join12(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
4873
+ const journal = JSON.parse(await readFile12(join13(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
4095
4874
  if (journal.state === "committed") {
4096
4875
  return { transactionId, status: "committed" };
4097
4876
  }
@@ -4118,7 +4897,7 @@ async function listTransactions(projectRoot) {
4118
4897
  const transactions = [];
4119
4898
  for (const transactionId of names) {
4120
4899
  try {
4121
- const journal = JSON.parse(await readFile11(join12(root, transactionId, "journal.json"), "utf8"));
4900
+ const journal = JSON.parse(await readFile12(join13(root, transactionId, "journal.json"), "utf8"));
4122
4901
  transactions.push({
4123
4902
  transactionId,
4124
4903
  kind: journal.kind,
@@ -4133,7 +4912,7 @@ async function listTransactions(projectRoot) {
4133
4912
  async function pendingTransactions(projectRoot) {
4134
4913
  return (await listTransactions(projectRoot)).filter((item2) => RECOVERY_STATES.has(item2.state));
4135
4914
  }
4136
- async function pathExists(path) {
4915
+ async function pathExists2(path) {
4137
4916
  try {
4138
4917
  await stat3(path);
4139
4918
  return true;
@@ -4149,12 +4928,12 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
4149
4928
  if (latest === void 0) {
4150
4929
  throw new Error("no committed update transaction is available for rollback");
4151
4930
  }
4152
- const transactionRoot = join12(stateLayout(projectRoot).transactions, latest.transactionId);
4153
- const journal = JSON.parse(await readFile11(join12(transactionRoot, "journal.json"), "utf8"));
4154
- const after = JSON.parse(await readFile11(join12(transactionRoot, "after", "manifest.json"), "utf8"));
4931
+ const transactionRoot = join13(stateLayout(projectRoot).transactions, latest.transactionId);
4932
+ const journal = JSON.parse(await readFile12(join13(transactionRoot, "journal.json"), "utf8"));
4933
+ const after = JSON.parse(await readFile12(join13(transactionRoot, "after", "manifest.json"), "utf8"));
4155
4934
  for (const entry of after) {
4156
- const target = join12(projectRoot, entry.path);
4157
- const exists6 = await pathExists(target);
4935
+ const target = join13(projectRoot, entry.path);
4936
+ const exists6 = await pathExists2(target);
4158
4937
  if (exists6 !== entry.exists || exists6 && await sha256File(target) !== entry.hash) {
4159
4938
  throw new Error("cannot rollback dirty path: " + entry.path);
4160
4939
  }
@@ -4166,10 +4945,10 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
4166
4945
  continue;
4167
4946
  }
4168
4947
  seen.add(snapshot.path);
4169
- const target = join12(projectRoot, snapshot.path);
4170
- const exists6 = await pathExists(target);
4948
+ const target = join13(projectRoot, snapshot.path);
4949
+ const exists6 = await pathExists2(target);
4171
4950
  if (snapshot.existed && snapshot.snapshot_name !== null) {
4172
- const content = await readFile11(join12(transactionRoot, "before", snapshot.snapshot_name));
4951
+ const content = await readFile12(join13(transactionRoot, "before", snapshot.snapshot_name));
4173
4952
  operations.push({
4174
4953
  operation: exists6 ? "modify" : "add",
4175
4954
  path: snapshot.path,
@@ -4198,7 +4977,7 @@ async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Dat
4198
4977
  if (!removable) {
4199
4978
  continue;
4200
4979
  }
4201
- await rm6(join12(stateLayout(projectRoot).transactions, item2.transactionId), {
4980
+ await rm7(join13(stateLayout(projectRoot).transactions, item2.transactionId), {
4202
4981
  recursive: true,
4203
4982
  force: true
4204
4983
  });
@@ -4207,31 +4986,9 @@ async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Dat
4207
4986
  return removed;
4208
4987
  }
4209
4988
 
4210
- // ../core/dist/update/conflicts.js
4211
- function operationTargetPath(operation) {
4212
- return operation.operation === "rename" ? operation.to_path : operation.path;
4213
- }
4214
- function operationSourcePath(operation) {
4215
- return operation.operation === "rename" ? operation.from_path : operation.path;
4216
- }
4217
- function operationAlreadyApplied(operation, baseline, projectVersion) {
4218
- const target = baseline.files[operationTargetPath(operation)];
4219
- if (operation.operation === "delete") {
4220
- return target?.deleted === true && target.last_applied_version === projectVersion;
4221
- }
4222
- return target?.deleted === false && target?.last_applied_version === projectVersion && target.baseline_hash === operation.content_sha256;
4223
- }
4224
- function managedBlockDirty(currentContent, managedBlockHash) {
4225
- if (managedBlockHash === void 0) {
4226
- return true;
4227
- }
4228
- const block = extractManagedBlock(currentContent);
4229
- return block === null || sha256Bytes(block) !== managedBlockHash;
4230
- }
4231
-
4232
4989
  // ../core/dist/update/update.js
4233
- import { lstat as lstat3, readFile as readFile12, rm as rm7 } from "node:fs/promises";
4234
- import { join as join13, resolve as resolve6 } from "node:path";
4990
+ import { readFile as readFile13 } from "node:fs/promises";
4991
+ import { join as join14, resolve as resolve7 } from "node:path";
4235
4992
  import { parse as parseYaml8 } from "yaml";
4236
4993
  var UpdateWorkflowError = class extends Error {
4237
4994
  exitCode;
@@ -4243,90 +5000,11 @@ var UpdateWorkflowError = class extends Error {
4243
5000
  this.code = code;
4244
5001
  }
4245
5002
  };
4246
- async function pathExists2(path) {
4247
- try {
4248
- await lstat3(path);
4249
- return true;
4250
- } catch (error) {
4251
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
4252
- return false;
4253
- }
4254
- throw error;
4255
- }
4256
- }
4257
- async function optionalContent(path) {
4258
- return await pathExists2(path) ? readFile12(path, "utf8") : null;
4259
- }
4260
- function manifestPayloadHash(manifest) {
4261
- const payload = { ...manifest };
4262
- delete payload.manifest_sha256;
4263
- return sha256Bytes(canonicalJson(payload));
4264
- }
4265
- function expectedBase(operation) {
4266
- return operation.operation === "add" ? null : operation.base_content_sha256;
4267
- }
4268
- function contentHash(operation) {
4269
- return operation.operation === "delete" ? null : operation.content_sha256;
4270
- }
4271
- async function loadBlob(root, client, artifactId, operation, requestId, dryRun) {
4272
- if (operation.operation === "delete") {
4273
- return null;
4274
- }
4275
- const hash = operation.content_sha256;
4276
- const cacheRoot = join13(root, ".harness", "cache", "server-artifacts", artifactId);
4277
- const cachePath = join13(cacheRoot, "blobs", hash.replace(":", "_"));
4278
- if (await pathExists2(cachePath) && await sha256File(cachePath) === hash) {
4279
- return readFile12(cachePath, "utf8");
4280
- }
4281
- const bytes = await client.downloadArtifactBlob(artifactId, hash, requestId);
4282
- if (bytes.byteLength !== operation.size_bytes || sha256Bytes(bytes) !== hash) {
4283
- await rm7(cachePath, { force: true });
4284
- throw new UpdateWorkflowError("artifact blob size or hash mismatch", 4, "ARTIFACT_HASH_MISMATCH");
4285
- }
4286
- if (!dryRun) {
4287
- await atomicWriteFile(cachePath, bytes);
4288
- }
4289
- return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
4290
- }
4291
- function nextBaselineEntry(operation, finalContent, projectVersion) {
4292
- const block = finalContent === null ? null : extractManagedBlock(finalContent);
4293
- return {
4294
- baseline_hash: contentHash(operation),
4295
- local_hash_at_apply: finalContent === null ? null : sha256Bytes(finalContent),
4296
- file_kind: operation.file_kind,
4297
- last_applied_version: projectVersion,
4298
- deleted: operation.operation === "delete",
4299
- ...block === null ? {} : { managed_block_hash: sha256Bytes(block) }
4300
- };
4301
- }
4302
- function transactionOperation(item2) {
4303
- if (item2.equivalent) {
4304
- return null;
4305
- }
4306
- const operation = item2.operation;
4307
- const target = operationTargetPath(operation);
4308
- if (operation.operation === "delete") {
4309
- return item2.finalContent === null ? { operation: "delete", path: target } : { operation: "modify", path: target, content: item2.finalContent };
4310
- }
4311
- if (operation.operation === "rename") {
4312
- return {
4313
- operation: "rename",
4314
- from_path: operation.from_path,
4315
- to_path: operation.to_path,
4316
- content: item2.finalContent ?? item2.content ?? ""
4317
- };
4318
- }
4319
- return {
4320
- operation: "modify",
4321
- path: target,
4322
- content: item2.finalContent ?? item2.content ?? ""
4323
- };
4324
- }
4325
5003
  async function updateProject(options) {
4326
- const root = resolve6(options.projectRoot);
5004
+ const root = resolve7(options.projectRoot);
4327
5005
  let project;
4328
5006
  try {
4329
- project = projectConfigSchema.parse(parseYaml8(await readFile12(join13(root, ".harness", "project.yaml"), "utf8")));
5007
+ project = projectConfigSchema.parse(parseYaml8(await readFile13(join14(root, ".harness", "project.yaml"), "utf8")));
4330
5008
  } catch {
4331
5009
  throw new UpdateWorkflowError("project configuration is invalid", 3, "PROJECT_CONFIG_INVALID");
4332
5010
  }
@@ -4365,183 +5043,18 @@ async function updateProject(options) {
4365
5043
  ...options.fetch === void 0 ? {} : { fetch: options.fetch }
4366
5044
  });
4367
5045
  try {
4368
- const discovery = await client.getUpdateManifest(project.project.project_id, {
4369
- base_project_version: baseline.complete_project_version,
4370
- base_manifest_hash: sha256Bytes(canonicalJson(baseline)),
4371
- adapter: project.adapters.enabled[0] ?? "claude-code",
4372
- profile: project.project.profiles[0] ?? "general"
4373
- }, requestId);
4374
- if (!discovery.delta_available || discovery.artifact_id === null) {
4375
- return {
4376
- requestId,
4377
- projectId: project.project.project_id,
4378
- artifactId: null,
4379
- observedProjectVersion: discovery.observed_project_version,
4380
- operations: [],
4381
- applied: [],
4382
- skipped: [],
4383
- transactionId: null,
4384
- dryRun: options.dryRun
4385
- };
4386
- }
4387
- const manifest = artifactManifestSchema.parse(await client.getArtifactManifest(discovery.artifact_id, requestId));
4388
- if (manifest.artifact_id !== discovery.artifact_id || manifest.project_id !== project.project.project_id || manifest.project_version === null || manifestPayloadHash(manifest) !== manifest.manifest_sha256) {
4389
- throw new UpdateWorkflowError("artifact manifest integrity check failed", 4, "ARTIFACT_HASH_MISMATCH");
4390
- }
4391
- const blobs = /* @__PURE__ */ new Map();
4392
- for (const operation of manifest.files) {
4393
- const policy = classifyFile(operationTargetPath(operation));
4394
- if (decideUpdate(policy, false).apply) {
4395
- blobs.set(operation, await loadBlob(root, client, manifest.artifact_id, operation, requestId, options.dryRun));
4396
- }
4397
- }
4398
- const prepared = [];
4399
- const skipped = [];
4400
- for (const operation of manifest.files) {
4401
- if (operationAlreadyApplied(operation, baseline, manifest.project_version)) {
4402
- continue;
4403
- }
4404
- const source = operationSourcePath(operation);
4405
- const target = operationTargetPath(operation);
4406
- const policy = classifyFile(target);
4407
- const staticDecision = decideUpdate(policy, false);
4408
- if (!staticDecision.apply) {
4409
- skipped.push({ path: target, operation: operation.operation, reason: staticDecision.reason });
4410
- continue;
4411
- }
4412
- const previous = baseline.files[source];
4413
- if (expectedBase(operation) !== (previous?.baseline_hash ?? null)) {
4414
- skipped.push({ path: target, operation: operation.operation, reason: "baseline-diverged" });
4415
- continue;
4416
- }
4417
- const sourceContent = await optionalContent(join13(root, source));
4418
- const targetContent = target === source ? sourceContent : await optionalContent(join13(root, target));
4419
- const incoming = blobs.get(operation) ?? null;
4420
- const incomingHash = contentHash(operation);
4421
- let equivalent = incomingHash !== null && targetContent !== null && sha256Bytes(targetContent) === incomingHash;
4422
- let dirty = false;
4423
- if (operation.operation === "add") {
4424
- if (targetContent !== null && !equivalent) {
4425
- if (policy.update_policy === "managed-block-only" && incoming !== null) {
4426
- const incomingBlock = extractManagedBlock(incoming) ?? incoming.trim();
4427
- equivalent = extractManagedBlock(targetContent) === incomingBlock;
4428
- dirty = !equivalent;
4429
- } else {
4430
- dirty = true;
4431
- }
4432
- }
4433
- } else if (sourceContent === null) {
4434
- dirty = operation.operation !== "delete";
4435
- } else if (policy.update_policy === "managed-block-only") {
4436
- dirty = previous?.managed_block_hash === void 0 ? (previous?.local_hash_at_apply ?? previous?.baseline_hash) !== sha256Bytes(sourceContent) : managedBlockDirty(sourceContent, previous.managed_block_hash);
4437
- } else {
4438
- dirty = (previous?.local_hash_at_apply ?? previous?.baseline_hash) !== sha256Bytes(sourceContent);
4439
- }
4440
- if (operation.operation === "rename" && targetContent !== null && !equivalent) {
4441
- dirty = true;
4442
- }
4443
- if (dirty) {
4444
- skipped.push({ path: target, operation: operation.operation, reason: "local-dirty" });
4445
- continue;
4446
- }
4447
- let finalContent = incoming;
4448
- if (policy.update_policy === "managed-block-only") {
4449
- if (operation.operation === "delete") {
4450
- finalContent = sourceContent === null ? null : removeManagedBlock(sourceContent);
4451
- equivalent = finalContent === sourceContent;
4452
- } else if (incoming !== null) {
4453
- const incomingBlock = extractManagedBlock(incoming) ?? incoming.trim();
4454
- const incomingId = extractSingleManagedBlockById(incoming)?.id;
4455
- const blockId = operation.operation === "add" || operation.operation === "modify" ? operation.block_id ?? incomingId : void 0;
4456
- finalContent = blockId !== void 0 ? upsertManagedBlockById(targetContent ?? "", blockId, incomingBlock) : upsertManagedBlock(targetContent ?? "", incomingBlock);
4457
- equivalent = finalContent === targetContent;
4458
- }
4459
- } else if (operation.operation === "delete" && sourceContent === null) {
4460
- equivalent = true;
4461
- }
4462
- prepared.push({ operation, content: incoming, finalContent, equivalent });
4463
- }
4464
- const applied = prepared.map((item2) => operationTargetPath(item2.operation));
4465
- if (options.dryRun) {
4466
- return {
4467
- requestId,
4468
- projectId: project.project.project_id,
4469
- artifactId: manifest.artifact_id,
4470
- observedProjectVersion: manifest.project_version,
4471
- operations: manifest.files,
4472
- applied,
4473
- skipped,
4474
- transactionId: null,
4475
- dryRun: true
4476
- };
4477
- }
4478
- await atomicWriteJson(join13(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
4479
- const lock = await acquireProtocolLock(root, "update", { requestId });
4480
- try {
4481
- const nextBaseline = baselineManifestSchema.parse(structuredClone(baseline));
4482
- for (const item2 of prepared) {
4483
- const target = operationTargetPath(item2.operation);
4484
- nextBaseline.files[target] = nextBaselineEntry(item2.operation, item2.finalContent, manifest.project_version);
4485
- if (item2.operation.operation === "rename") {
4486
- nextBaseline.files[item2.operation.from_path] = {
4487
- baseline_hash: null,
4488
- local_hash_at_apply: null,
4489
- file_kind: item2.operation.file_kind,
4490
- last_applied_version: manifest.project_version,
4491
- deleted: true
4492
- };
4493
- }
4494
- }
4495
- if (skipped.length === 0) {
4496
- nextBaseline.complete_project_version = manifest.project_version;
4497
- nextBaseline.artifact_manifest_hash = manifest.manifest_sha256;
4498
- nextBaseline.latest_artifact_id = manifest.artifact_id;
4499
- }
4500
- const transactionId = "tx_update_" + Date.now() + "_" + uuidV7();
4501
- const reportPath = ".harness/reports/update-" + requestId + ".json";
4502
- const report = {
4503
- schema_version: 1,
4504
- request_id: requestId,
4505
- artifact_id: manifest.artifact_id,
4506
- observed_project_version: manifest.project_version,
4507
- status: skipped.length === 0 ? "applied" : "partial_due_to_dirty",
4508
- applied,
4509
- skipped,
4510
- transaction_id: transactionId
4511
- };
4512
- const operations = prepared.map((item2) => transactionOperation(item2)).filter((item2) => item2 !== null);
4513
- operations.push({
4514
- operation: "modify",
4515
- path: ".harness/state/baseline/manifest.json",
4516
- content: JSON.stringify(nextBaseline, null, 2) + "\n"
4517
- }, {
4518
- operation: "add",
4519
- path: reportPath,
4520
- content: JSON.stringify(report, null, 2) + "\n"
4521
- }, {
4522
- operation: "modify",
4523
- path: ".harness/state/local/last-update.json",
4524
- content: JSON.stringify(report, null, 2) + "\n"
4525
- });
4526
- await runTransaction(root, operations, {
4527
- id: transactionId,
4528
- kind: "update",
4529
- ...options.transactionOptions ?? {}
4530
- });
4531
- return {
4532
- requestId,
4533
- projectId: project.project.project_id,
4534
- artifactId: manifest.artifact_id,
4535
- observedProjectVersion: manifest.project_version,
4536
- operations: manifest.files,
4537
- applied,
4538
- skipped,
4539
- transactionId,
4540
- dryRun: false
4541
- };
4542
- } finally {
4543
- await lock.release();
4544
- }
5046
+ const result = await synchronizeArtifacts({
5047
+ projectRoot: root,
5048
+ project,
5049
+ client,
5050
+ requestId,
5051
+ dryRun: options.dryRun,
5052
+ ...options.conflictStrategy === void 0 ? {} : { conflictStrategy: options.conflictStrategy },
5053
+ ...options.resolveOverrides === void 0 ? {} : { resolveOverrides: options.resolveOverrides },
5054
+ ...options.confirmConflictStrategy === void 0 ? {} : { confirmConflictStrategy: options.confirmConflictStrategy },
5055
+ ...options.transactionOptions === void 0 ? {} : { transactionOptions: options.transactionOptions }
5056
+ }, baseline);
5057
+ return result;
4545
5058
  } catch (error) {
4546
5059
  if (error instanceof UpdateWorkflowError) {
4547
5060
  throw error;
@@ -4549,16 +5062,25 @@ async function updateProject(options) {
4549
5062
  if (error instanceof ApiError) {
4550
5063
  throw new UpdateWorkflowError(error.message, error.status === 401 || error.status === 403 ? 8 : error.status === 409 ? 5 : 4, error.code);
4551
5064
  }
5065
+ if (error instanceof Error && error.message === "ARTIFACT_HASH_MISMATCH") {
5066
+ throw new UpdateWorkflowError("artifact blob size or hash mismatch", 4, "ARTIFACT_HASH_MISMATCH");
5067
+ }
4552
5068
  if (error instanceof Error && error.name === "ZodError") {
4553
5069
  throw new UpdateWorkflowError("artifact schema validation failed", 7, "SCHEMA_VALIDATION_FAILED");
4554
5070
  }
5071
+ if (error instanceof Error && error.message.startsWith("DUPLICATE_ARTIFACT_ID")) {
5072
+ throw new UpdateWorkflowError(error.message, 4, "DUPLICATE_ARTIFACT_ID");
5073
+ }
5074
+ if (error instanceof Error && error.message === "MAX_SYNC_ARTIFACT_ITERATIONS_EXCEEDED") {
5075
+ throw new UpdateWorkflowError(error.message, 4, "SYNC_ITERATION_LIMIT");
5076
+ }
4555
5077
  throw new UpdateWorkflowError(error instanceof Error ? error.message : "update failed", 4, "NETWORK_OR_SERVER_ERROR");
4556
5078
  }
4557
5079
  }
4558
5080
 
4559
5081
  // src/config/init-config.ts
4560
- import { isAbsolute as isAbsolute2, join as join14 } from "node:path";
4561
- import { readFile as readFile13 } from "node:fs/promises";
5082
+ import { isAbsolute as isAbsolute2, join as join15 } from "node:path";
5083
+ import { readFile as readFile14 } from "node:fs/promises";
4562
5084
  var InitConfigurationError = class extends Error {
4563
5085
  exitCode;
4564
5086
  code;
@@ -4641,9 +5163,9 @@ function hasOwn(record, key) {
4641
5163
  async function resolveInitConfig(cwd, flags, prompts = {}, warnings = []) {
4642
5164
  let fileConfig = {};
4643
5165
  if (flags.config !== void 0) {
4644
- const path = isAbsolute2(flags.config) ? flags.config : join14(cwd, flags.config);
5166
+ const path = isAbsolute2(flags.config) ? flags.config : join15(cwd, flags.config);
4645
5167
  try {
4646
- fileConfig = JSON.parse(await readFile13(path, "utf8"));
5168
+ fileConfig = JSON.parse(await readFile14(path, "utf8"));
4647
5169
  } catch (error) {
4648
5170
  throw new InitConfigurationError(
4649
5171
  "unable to read init config: " + (error instanceof Error ? error.message : String(error)),
@@ -4725,8 +5247,8 @@ function serializeCliResult(result) {
4725
5247
  }
4726
5248
 
4727
5249
  // src/config/codebuddy-setup.ts
4728
- import { mkdir as mkdir4, readFile as readFile14, readdir as readdir7, stat as stat4, writeFile as writeFile5 } from "node:fs/promises";
4729
- import { basename as basename2, dirname as dirname4, extname, join as join15 } from "node:path";
5250
+ import { mkdir as mkdir4, readFile as readFile15, readdir as readdir7, stat as stat4, writeFile as writeFile5 } from "node:fs/promises";
5251
+ import { basename as basename2, dirname as dirname4, extname, join as join16 } from "node:path";
4730
5252
  var MANAGED_RULE_NAMES = /* @__PURE__ */ new Set([
4731
5253
  "harness-general.md",
4732
5254
  "harness-general.mdc",
@@ -4745,7 +5267,7 @@ async function exists4(path) {
4745
5267
  }
4746
5268
  async function readJsonObject(path) {
4747
5269
  try {
4748
- const parsed = JSON.parse(await readFile14(path, "utf8"));
5270
+ const parsed = JSON.parse(await readFile15(path, "utf8"));
4749
5271
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
4750
5272
  } catch (error) {
4751
5273
  if (error instanceof Error && "code" in error && error.code === "ENOENT") return {};
@@ -4753,27 +5275,27 @@ async function readJsonObject(path) {
4753
5275
  }
4754
5276
  }
4755
5277
  async function inspectCodeBuddySetup(projectRoot) {
4756
- const rulesRoot = join15(projectRoot, ".claude", "rules");
5278
+ const rulesRoot = join16(projectRoot, ".claude", "rules");
4757
5279
  let claudeRules = [];
4758
5280
  try {
4759
5281
  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();
4760
5282
  } catch (error) {
4761
5283
  if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
4762
5284
  }
4763
- const mcp = await readJsonObject(join15(projectRoot, ".mcp.json"));
5285
+ const mcp = await readJsonObject(join16(projectRoot, ".mcp.json"));
4764
5286
  const servers = mcp?.mcpServers;
4765
5287
  const configured = servers !== null && typeof servers === "object" && !Array.isArray(servers) && Object.prototype.hasOwnProperty.call(servers, "codegraph");
4766
5288
  return {
4767
5289
  claudeRules,
4768
- hasCodeGraphIndex: await exists4(join15(projectRoot, ".codegraph")),
5290
+ hasCodeGraphIndex: await exists4(join16(projectRoot, ".codegraph")),
4769
5291
  codeGraphConfigured: configured
4770
5292
  };
4771
5293
  }
4772
5294
  function destinationTargets(root, surface, name) {
4773
5295
  const stem = basename2(name, extname(name));
4774
5296
  const targets = [];
4775
- if (surface !== "cli") targets.push(join15(root, ".codebuddy", ".rules", `${stem}.mdc`));
4776
- if (surface !== "ide") targets.push(join15(root, ".codebuddy", "rules", `${stem}.md`));
5297
+ if (surface !== "cli") targets.push(join16(root, ".codebuddy", ".rules", `${stem}.mdc`));
5298
+ if (surface !== "ide") targets.push(join16(root, ".codebuddy", "rules", `${stem}.md`));
4777
5299
  return targets;
4778
5300
  }
4779
5301
  async function applyCodeBuddySetup(options) {
@@ -4787,8 +5309,8 @@ async function applyCodeBuddySetup(options) {
4787
5309
  if (options.syncClaudeRules) {
4788
5310
  const plan = await inspectCodeBuddySetup(options.projectRoot);
4789
5311
  for (const name of plan.claudeRules) {
4790
- const source = join15(options.projectRoot, ".claude", "rules", name);
4791
- const content = await readFile14(source, "utf8");
5312
+ const source = join16(options.projectRoot, ".claude", "rules", name);
5313
+ const content = await readFile15(source, "utf8");
4792
5314
  if (SENSITIVE_ASSIGNMENT.test(content) || PRIVATE_KEY_BLOCK.test(content)) {
4793
5315
  result.skippedSensitive.push(name);
4794
5316
  continue;
@@ -4805,7 +5327,7 @@ async function applyCodeBuddySetup(options) {
4805
5327
  }
4806
5328
  }
4807
5329
  if (options.configureCodeGraph) {
4808
- const path = join15(options.projectRoot, ".mcp.json");
5330
+ const path = join16(options.projectRoot, ".mcp.json");
4809
5331
  const current = await readJsonObject(path);
4810
5332
  if (current === null) {
4811
5333
  result.warnings.push(".mcp.json \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u5DF2\u4FDD\u7559\u539F\u6587\u4EF6\u5E76\u8DF3\u8FC7 CodeGraph MCP \u914D\u7F6E");
@@ -4829,13 +5351,13 @@ async function applyCodeBuddySetup(options) {
4829
5351
  }
4830
5352
 
4831
5353
  // src/commands/refresh.ts
4832
- import { readFile as readFile15 } from "node:fs/promises";
4833
- import { join as join16 } from "node:path";
5354
+ import { readFile as readFile16 } from "node:fs/promises";
5355
+ import { join as join17 } from "node:path";
4834
5356
  import { parse as parseYaml9 } from "yaml";
4835
5357
  async function detectProject(root) {
4836
5358
  let content;
4837
5359
  try {
4838
- content = await readFile15(join16(root, ".harness", "project.yaml"), "utf8");
5360
+ content = await readFile16(join17(root, ".harness", "project.yaml"), "utf8");
4839
5361
  } catch (error) {
4840
5362
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
4841
5363
  return { status: "absent" };
@@ -5317,6 +5839,24 @@ async function runCleanup(options, dependencies) {
5317
5839
  }
5318
5840
 
5319
5841
  // src/commands/push.ts
5842
+ var CONFLICT_PREVIEW_LIMIT2 = 8;
5843
+ async function promptStaleConflictStrategy(conflicts, dependencies) {
5844
+ const lines = [`\u68C0\u6D4B\u5230 ${conflicts.length} \u4E2A\u670D\u52A1\u7AEF\u5E76\u53D1\u51B2\u7A81\uFF1A`];
5845
+ for (const conflict2 of conflicts.slice(0, CONFLICT_PREVIEW_LIMIT2)) {
5846
+ lines.push(` - ${conflict2.path}`);
5847
+ }
5848
+ if (conflicts.length > CONFLICT_PREVIEW_LIMIT2) {
5849
+ lines.push(` - \u2026\u53E6 ${conflicts.length - CONFLICT_PREVIEW_LIMIT2} \u4E2A`);
5850
+ }
5851
+ dependencies.stderr(lines.join("\n") + "\n");
5852
+ const answer = await dependencies.prompt(
5853
+ "\u8BF7\u9009\u62E9\u5904\u7406\u65B9\u5F0F\uFF1A\n 1. \u4FDD\u7559\u672C\u5730\u5185\u5BB9\u5E76\u7EE7\u7EED\u63A8\u9001\uFF08\u63A8\u8350\uFF09\n 2. \u63A5\u53D7\u670D\u52A1\u7AEF\u5185\u5BB9\u5E76\u7EE7\u7EED\u63A8\u9001\n 3. \u9000\u51FA\uFF0C\u7A0D\u540E\u9010\u9879\u5904\u7406\n\u8BF7\u9009\u62E9 [1]\uFF1A"
5854
+ );
5855
+ const selected = answer.trim();
5856
+ if (selected === "" || selected === "1") return "keep-local";
5857
+ if (selected === "2") return "accept-remote";
5858
+ return false;
5859
+ }
5320
5860
  function formatFindings(details) {
5321
5861
  if (details?.findings === void 0 || details.findings.length === 0) {
5322
5862
  return "";
@@ -5417,7 +5957,8 @@ async function runPush(options, dependencies) {
5417
5957
  const reasonAnswer = await dependencies.prompt("\u8DF3\u8FC7\u539F\u56E0\uFF08\u53EF\u9009\uFF0C\u56DE\u8F66\u8DF3\u8FC7\uFF09: ");
5418
5958
  const reason = reasonAnswer.trim();
5419
5959
  return reason === "" ? { skip: true } : { skip: true, reason };
5420
- } }
5960
+ } },
5961
+ ...options.nonInteractive === true ? {} : { confirmConflictStrategy: async (conflicts) => promptStaleConflictStrategy(conflicts, dependencies) }
5421
5962
  });
5422
5963
  if ("cancelled" in result && result.cancelled === true) {
5423
5964
  return 2;
@@ -5449,7 +5990,7 @@ async function runPush(options, dependencies) {
5449
5990
  warnings: result.preview.skipped,
5450
5991
  errors: []
5451
5992
  };
5452
- dependencies.stdout(options.json === true ? serializeCliResult(output) : options.dryRun === true ? "Push preview contains " + items.length + " operations.\n" : "Pushed artifact " + result.artifactId + " (proposal " + result.proposalId + ").\n");
5993
+ dependencies.stdout(options.json === true ? serializeCliResult(output) : options.dryRun === true ? "Push preview contains " + items.length + " operations.\n" : "Pushed artifact " + ("artifactId" in result ? String(result.artifactId) : "unknown") + " (proposal " + result.proposalId + ").\n");
5453
5994
  return 0;
5454
5995
  } catch (error) {
5455
5996
  if (attempt === 0 && options.nonInteractive !== true && options.dryRun !== true && error instanceof PushWorkflowError && (error.code === "SERVER_URL_REQUIRED" || error.code === "TOKEN_INVALID")) {
@@ -5509,19 +6050,46 @@ async function runPush(options, dependencies) {
5509
6050
  }
5510
6051
 
5511
6052
  // src/commands/update.ts
6053
+ function parseResolveOverrides(entries) {
6054
+ const map = /* @__PURE__ */ new Map();
6055
+ for (const entry of entries ?? []) {
6056
+ const eq = entry.indexOf("=");
6057
+ if (eq <= 0) {
6058
+ throw new UpdateWorkflowError(
6059
+ "invalid --resolve value; expected path=keep-local|accept-remote",
6060
+ 3,
6061
+ "RESOLVE_OPTION_INVALID"
6062
+ );
6063
+ }
6064
+ const path = entry.slice(0, eq).trim();
6065
+ const strategy = entry.slice(eq + 1).trim();
6066
+ if (path.length === 0 || strategy !== "keep-local" && strategy !== "accept-remote") {
6067
+ throw new UpdateWorkflowError(
6068
+ "invalid --resolve value; expected path=keep-local|accept-remote",
6069
+ 3,
6070
+ "RESOLVE_OPTION_INVALID"
6071
+ );
6072
+ }
6073
+ map.set(path, strategy);
6074
+ }
6075
+ return map;
6076
+ }
5512
6077
  async function runUpdate(options, dependencies) {
5513
6078
  const requestId = uuidV7();
5514
6079
  if (options.nonInteractive === true && options.yes !== true && options.dryRun !== true) {
5515
6080
  dependencies.stderr("\u975E\u4EA4\u4E92\u6A21\u5F0F\u66F4\u65B0\u9700\u8981 --yes\n");
5516
6081
  return 2;
5517
6082
  }
6083
+ const resolveOverrides = parseResolveOverrides(options.resolve);
5518
6084
  const execute = async (dryRun) => updateProject({
5519
6085
  projectRoot: dependencies.cwd,
5520
6086
  ...options.serverUrl === void 0 ? {} : { serverUrl: options.serverUrl },
5521
6087
  ...options.tokenEnv === void 0 ? {} : { tokenEnv: options.tokenEnv },
5522
6088
  env: dependencies.env,
5523
6089
  dryRun,
5524
- fetch: dependencies.fetch
6090
+ fetch: dependencies.fetch,
6091
+ ...options.conflictStrategy === void 0 ? {} : { conflictStrategy: options.conflictStrategy },
6092
+ ...resolveOverrides.size === 0 ? {} : { resolveOverrides }
5525
6093
  });
5526
6094
  try {
5527
6095
  let result;
@@ -5539,22 +6107,24 @@ async function runUpdate(options, dependencies) {
5539
6107
  }
5540
6108
  const applied = new Set(result.applied);
5541
6109
  const skippedByPath = new Map(result.skipped.map((item2) => [item2.path, item2]));
6110
+ const acknowledgedByPath = new Map(result.acknowledged.map((item2) => [item2.path, item2]));
5542
6111
  const items = result.operations.map((operation) => {
5543
6112
  const path = operation.operation === "rename" ? operation.to_path : operation.path;
5544
6113
  const skipped = skippedByPath.get(path);
6114
+ const acknowledged = acknowledgedByPath.get(path);
5545
6115
  return {
5546
6116
  path,
5547
6117
  operation: operation.operation,
5548
6118
  file_kind: operation.file_kind,
5549
6119
  policy: "update",
5550
- status: skipped !== void 0 ? "skipped" : applied.has(path) ? options.dryRun === true ? "planned" : "applied" : "already-applied",
5551
- reason: skipped?.reason ?? null,
6120
+ status: skipped !== void 0 ? "skipped" : acknowledged !== void 0 ? "acknowledged" : applied.has(path) ? options.dryRun === true ? "planned" : "applied" : "already-applied",
6121
+ reason: skipped?.reason ?? acknowledged?.reason ?? null,
5552
6122
  size_bytes: "size_bytes" in operation ? operation.size_bytes : 0
5553
6123
  };
5554
6124
  });
5555
- const exitCode = result.skipped.length > 0 ? 5 : 0;
6125
+ const exitCode = result.conflicts.length > 0 ? 5 : 0;
5556
6126
  const output = {
5557
- schema_version: 1,
6127
+ schema_version: 2,
5558
6128
  command: "update",
5559
6129
  request_id: requestId,
5560
6130
  dry_run: options.dryRun === true,
@@ -5565,13 +6135,15 @@ async function runUpdate(options, dependencies) {
5565
6135
  discovered: result.operations.length,
5566
6136
  applied: options.dryRun === true ? 0 : result.applied.length,
5567
6137
  planned: options.dryRun === true ? result.applied.length : 0,
6138
+ acknowledged: result.acknowledged.length,
6139
+ resolved: result.resolvedKeepLocal.length + result.resolvedAcceptRemote.length,
5568
6140
  skipped: result.skipped.length
5569
6141
  },
5570
6142
  items,
5571
- warnings: result.skipped,
6143
+ warnings: [...result.acknowledged, ...result.skipped],
5572
6144
  errors: []
5573
6145
  };
5574
- dependencies.stdout(options.json === true ? serializeCliResult(output) : result.artifactId === null ? "\u6CA1\u6709\u53EF\u5E94\u7528\u7684\u5DF2\u6279\u51C6\u66F4\u65B0\u3002\n" : "\u66F4\u65B0\u5B8C\u6210\uFF1A\u5DF2\u5E94\u7528 " + result.applied.length + " \u4E2A\u6761\u76EE\uFF0C\u8DF3\u8FC7 " + result.skipped.length + " \u4E2A\u3002\n");
6146
+ dependencies.stdout(options.json === true ? serializeCliResult(output) : result.artifactId === null ? "\u6CA1\u6709\u53EF\u5E94\u7528\u7684\u5DF2\u6279\u51C6\u66F4\u65B0\u3002\n" : "\u66F4\u65B0\u5B8C\u6210\uFF1A\u5DF2\u5E94\u7528 " + result.applied.length + " \u4E2A\u6761\u76EE\uFF0C\u5DF2\u786E\u8BA4 " + result.acknowledged.length + " \u4E2A\uFF0C\u51B2\u7A81 " + result.conflicts.length + " \u4E2A\u3002\n");
5575
6147
  return exitCode;
5576
6148
  } catch (error) {
5577
6149
  const exitCode = error instanceof UpdateWorkflowError ? error.exitCode : 1;
@@ -5601,7 +6173,7 @@ async function runUpdate(options, dependencies) {
5601
6173
 
5602
6174
  // src/commands/recovery.ts
5603
6175
  import { stat as stat5 } from "node:fs/promises";
5604
- import { join as join17 } from "node:path";
6176
+ import { join as join18 } from "node:path";
5605
6177
  async function exists5(path) {
5606
6178
  try {
5607
6179
  await stat5(path);
@@ -5661,7 +6233,7 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
5661
6233
  return 5;
5662
6234
  }
5663
6235
  }
5664
- const initialized = await exists5(join17(dependencies.cwd, ".harness", "project.yaml"));
6236
+ const initialized = await exists5(join18(dependencies.cwd, ".harness", "project.yaml"));
5665
6237
  if (!initialized || options.nonInteractive === true || explicitConfigure(options)) {
5666
6238
  return null;
5667
6239
  }
@@ -5704,8 +6276,8 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
5704
6276
  }
5705
6277
 
5706
6278
  // src/workflow-data/resolve.ts
5707
- import { cp, mkdir as mkdir5, readdir as readdir8, readFile as readFile16, rm as rm8, stat as stat6 } from "node:fs/promises";
5708
- import { dirname as dirname5, join as join18, relative as relative2 } from "node:path";
6279
+ import { cp, mkdir as mkdir5, readdir as readdir8, readFile as readFile17, rm as rm8, stat as stat6 } from "node:fs/promises";
6280
+ import { dirname as dirname5, join as join19, relative as relative2 } from "node:path";
5709
6281
  import { fileURLToPath } from "node:url";
5710
6282
  var WorkflowDataResolutionError = class extends Error {
5711
6283
  code;
@@ -5742,14 +6314,14 @@ async function listFilesRecursive(root, base = root) {
5742
6314
  const entries = await readdir8(root, { withFileTypes: true });
5743
6315
  const files = [];
5744
6316
  for (const entry of entries) {
5745
- const absolute = join18(root, entry.name);
6317
+ const absolute = join19(root, entry.name);
5746
6318
  if (entry.isDirectory()) {
5747
6319
  files.push(...await listFilesRecursive(absolute, base));
5748
6320
  continue;
5749
6321
  }
5750
6322
  if (!entry.isFile()) continue;
5751
6323
  const rel = relative2(base, absolute).replaceAll("\\", "/");
5752
- files.push({ path: rel, content: await readFile16(absolute, "utf8") });
6324
+ files.push({ path: rel, content: await readFile17(absolute, "utf8") });
5753
6325
  }
5754
6326
  return files;
5755
6327
  }
@@ -5762,7 +6334,7 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
5762
6334
  const manifest = await readWorkflowFamilyManifest(resourcesRoot);
5763
6335
  const expected = manifest.content_sha256;
5764
6336
  if (typeof expected !== "string" || expected.length === 0) return;
5765
- const harnessRoot = join18(resourcesRoot, "harness");
6337
+ const harnessRoot = join19(resourcesRoot, "harness");
5766
6338
  if (!await pathExists3(harnessRoot)) {
5767
6339
  throw new WorkflowDataResolutionError(
5768
6340
  "\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u7F3A\u5C11 harness/\uFF0C\u65E0\u6CD5\u6821\u9A8C SHA-256",
@@ -5784,37 +6356,37 @@ async function monorepoResourcesRoot() {
5784
6356
  const here = dirname5(fileURLToPath(import.meta.url));
5785
6357
  const candidates = [
5786
6358
  // TypeScript source: packages/cli/src/workflow-data -> packages/workflow-data-harness.
5787
- join18(here, "../../../workflow-data-harness"),
6359
+ join19(here, "../../../workflow-data-harness"),
5788
6360
  // Bundled CLI: packages/cli/dist -> packages/workflow-data-harness.
5789
- join18(here, "../../workflow-data-harness"),
6361
+ join19(here, "../../workflow-data-harness"),
5790
6362
  // Test/build layouts that preserve additional source directory levels.
5791
- join18(here, "../../../../packages/workflow-data-harness")
6363
+ join19(here, "../../../../packages/workflow-data-harness")
5792
6364
  ];
5793
6365
  for (const candidate of candidates) {
5794
- if (await pathExists3(join18(candidate, "harness", "manifests"))) return candidate;
6366
+ if (await pathExists3(join19(candidate, "harness", "manifests"))) return candidate;
5795
6367
  }
5796
6368
  return null;
5797
6369
  }
5798
6370
  async function siblingWorkflowPackage(cwd) {
5799
6371
  const here = dirname5(fileURLToPath(import.meta.url));
5800
6372
  const candidates = [
5801
- join18(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
6373
+ join19(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
5802
6374
  // Published layout: node_modules/hunter-harness/dist/bin.js alongside the
5803
6375
  // scoped workflow package in node_modules/@hunter-harness/workflow-harness.
5804
- join18(here, "..", "..", "@hunter-harness", "workflow-harness"),
6376
+ join19(here, "..", "..", "@hunter-harness", "workflow-harness"),
5805
6377
  // Monorepo bundled layout: packages/cli/dist/bin.js.
5806
- join18(here, "..", "..", "workflow-data-harness")
6378
+ join19(here, "..", "..", "workflow-data-harness")
5807
6379
  ];
5808
6380
  for (const candidate of candidates) {
5809
- if (await pathExists3(join18(candidate, "harness", "manifests"))) return candidate;
6381
+ if (await pathExists3(join19(candidate, "harness", "manifests"))) return candidate;
5810
6382
  }
5811
6383
  return null;
5812
6384
  }
5813
6385
  async function readCachedNpmPackageVersion(cacheRoot) {
5814
- const manifestPath = join18(cacheRoot, "package.json");
6386
+ const manifestPath = join19(cacheRoot, "package.json");
5815
6387
  if (!await pathExists3(manifestPath)) return null;
5816
6388
  try {
5817
- const pkg = JSON.parse(await readFile16(manifestPath, "utf8"));
6389
+ const pkg = JSON.parse(await readFile17(manifestPath, "utf8"));
5818
6390
  return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : null;
5819
6391
  } catch {
5820
6392
  return null;
@@ -5837,12 +6409,12 @@ async function latestWorkflowCacheIsStale(cacheRoot, packageName, resolveManifes
5837
6409
  }
5838
6410
  async function materializeWorkflowPackageCache(cacheRoot, packageSpec, extract) {
5839
6411
  await mkdir5(cacheRoot, { recursive: true });
5840
- const staging = join18(cacheRoot, ".extract");
6412
+ const staging = join19(cacheRoot, ".extract");
5841
6413
  await rm8(staging, { recursive: true, force: true });
5842
6414
  await mkdir5(staging, { recursive: true });
5843
6415
  await extract(packageSpec, staging);
5844
- const packageDir = await pathExists3(join18(staging, "harness", "manifests")) ? staging : join18(staging, "package");
5845
- if (!await pathExists3(join18(packageDir, "harness", "manifests"))) {
6416
+ const packageDir = await pathExists3(join19(staging, "harness", "manifests")) ? staging : join19(staging, "package");
6417
+ if (!await pathExists3(join19(packageDir, "harness", "manifests"))) {
5846
6418
  throw new WorkflowDataResolutionError(
5847
6419
  "\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u5185\u5BB9\u65E0\u6548\uFF1A\u7F3A\u5C11 harness/manifests",
5848
6420
  "WORKFLOW_DATA_INVALID",
@@ -5871,8 +6443,8 @@ async function resolveWorkflowResourcesRoot(options, argv = []) {
5871
6443
  const packageName = workflowPackageName(family, options.env);
5872
6444
  const packageSpec = version === "latest" ? packageName : `${packageName}@${version}`;
5873
6445
  const cacheKey = packageSpec.replace("/", "+");
5874
- const cacheRoot = join18(options.cwd, ".harness", "cache", "workflow-packages", cacheKey);
5875
- if (await pathExists3(join18(cacheRoot, "harness", "manifests"))) {
6446
+ const cacheRoot = join19(options.cwd, ".harness", "cache", "workflow-packages", cacheKey);
6447
+ if (await pathExists3(join19(cacheRoot, "harness", "manifests"))) {
5876
6448
  if (version === "latest") {
5877
6449
  const stale = await latestWorkflowCacheIsStale(cacheRoot, packageName, options.pacoteManifest);
5878
6450
  if (stale) {
@@ -5918,9 +6490,9 @@ function describeWorkflowDataFetchFailure(error, packageSpec) {
5918
6490
  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}`;
5919
6491
  }
5920
6492
  async function readWorkflowFamilyManifest(resourcesRoot) {
5921
- const manifestPath = join18(resourcesRoot, "hunter-workflow-family.json");
6493
+ const manifestPath = join19(resourcesRoot, "hunter-workflow-family.json");
5922
6494
  if (!await pathExists3(manifestPath)) return {};
5923
- return JSON.parse(await readFile16(manifestPath, "utf8"));
6495
+ return JSON.parse(await readFile17(manifestPath, "utf8"));
5924
6496
  }
5925
6497
 
5926
6498
  // src/bin.ts
@@ -6011,7 +6583,15 @@ async function runCli(argv, overrides = {}) {
6011
6583
  dependencies
6012
6584
  );
6013
6585
  });
6014
- addCommonOptions(program.command("update")).description("\u5E94\u7528\u5DF2\u6279\u51C6\u7684\u670D\u52A1\u7AEF\u4EA7\u7269").action(async (options) => {
6586
+ addCommonOptions(program.command("update")).description("\u5E94\u7528\u5DF2\u6279\u51C6\u7684\u670D\u52A1\u7AEF\u4EA7\u7269").option(
6587
+ "--conflict-strategy <strategy>",
6588
+ "manual | keep-local | accept-remote\uFF08\u9ED8\u8BA4 manual\uFF1Brename \u51B2\u7A81\u59CB\u7EC8 manual\uFF09"
6589
+ ).option(
6590
+ "--resolve <path=strategy>",
6591
+ "\u5355\u8DEF\u5F84\u8986\u76D6 keep-local|accept-remote\uFF08\u53EF\u91CD\u590D\uFF09",
6592
+ (value, previous) => [...previous, value],
6593
+ []
6594
+ ).action(async (options) => {
6015
6595
  exitCode = await runUpdate(
6016
6596
  { ...program.opts(), ...options },
6017
6597
  dependencies