hunter-harness 0.2.11 → 0.2.13

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 +951 -401
  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) {
@@ -1230,6 +1230,9 @@ var HunterHarnessApiClient = class {
1230
1230
  body
1231
1231
  });
1232
1232
  }
1233
+ async getProject(projectId, requestId) {
1234
+ return this.request("GET", "/api/v1/projects/" + encodeURIComponent(projectId), { requestId });
1235
+ }
1233
1236
  async createProposalSession(projectId, body, requestId, idempotencyKey) {
1234
1237
  return this.request("POST", "/api/v1/projects/" + encodeURIComponent(projectId) + "/proposal-sessions", { requestId, idempotencyKey, body });
1235
1238
  }
@@ -3454,8 +3457,8 @@ function resolvePushAuth(input) {
3454
3457
  }
3455
3458
 
3456
3459
  // ../core/dist/push/push.js
3457
- import { lstat as lstat2, readFile as readFile9, readdir as readdir4, rm as rm4 } from "node:fs/promises";
3458
- 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";
3459
3462
  import { parse as parseYaml5, stringify as stringifyYaml4 } from "yaml";
3460
3463
 
3461
3464
  // ../core/dist/state/baseline.js
@@ -3516,6 +3519,659 @@ async function acquireProtocolLock(projectRoot, operation, options = {}) {
3516
3519
  throw new Error("unable to acquire protocol lock");
3517
3520
  }
3518
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
+
3519
4175
  // ../core/dist/push/push.js
3520
4176
  var PushWorkflowError = class extends Error {
3521
4177
  exitCode;
@@ -3542,7 +4198,7 @@ var SHARED_MANAGED_FILES = [
3542
4198
  ];
3543
4199
  async function exists3(path) {
3544
4200
  try {
3545
- await lstat2(path);
4201
+ await lstat3(path);
3546
4202
  return true;
3547
4203
  } catch (error) {
3548
4204
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -3556,7 +4212,7 @@ async function walkFiles(root, current, output) {
3556
4212
  return;
3557
4213
  }
3558
4214
  for (const item2 of await readdir4(current, { withFileTypes: true })) {
3559
- const path = join10(current, item2.name);
4215
+ const path = join11(current, item2.name);
3560
4216
  if (item2.isSymbolicLink()) {
3561
4217
  throw new PushWorkflowError("symlink is not pushable", 6, "UNSAFE_SYMLINK");
3562
4218
  }
@@ -3578,7 +4234,7 @@ async function walkHarnessEntries(root, directory, output) {
3578
4234
  return;
3579
4235
  for (const item2 of await readdir4(directory, { withFileTypes: true })) {
3580
4236
  if (item2.name.startsWith("harness-")) {
3581
- const path = join10(directory, item2.name);
4237
+ const path = join11(directory, item2.name);
3582
4238
  if (item2.isDirectory()) {
3583
4239
  await walkFiles(root, path, output);
3584
4240
  } else if (item2.isFile()) {
@@ -3588,7 +4244,7 @@ async function walkHarnessEntries(root, directory, output) {
3588
4244
  }
3589
4245
  }
3590
4246
  async function managedFiles(projectRoot, project) {
3591
- const root = resolve5(projectRoot);
4247
+ const root = resolve6(projectRoot);
3592
4248
  const paths = [];
3593
4249
  const adapters = getAdapters(enabledHarnessAgents(project));
3594
4250
  const managedFiles2 = [
@@ -3597,25 +4253,25 @@ async function managedFiles(projectRoot, project) {
3597
4253
  ...adapters.some((adapter) => adapter.name === "codebuddy") ? ["CODEBUDDY.md"] : []
3598
4254
  ];
3599
4255
  for (const path of managedFiles2) {
3600
- if (await exists3(join10(root, path))) {
4256
+ if (await exists3(join11(root, path))) {
3601
4257
  paths.push(path);
3602
4258
  }
3603
4259
  }
3604
4260
  for (const path of SHARED_MANAGED_ROOTS) {
3605
- await walkFiles(root, join10(root, path), paths);
4261
+ await walkFiles(root, join11(root, path), paths);
3606
4262
  }
3607
4263
  for (const adapter of adapters) {
3608
4264
  if (adapter.rulesRoot !== null) {
3609
- await walkFiles(root, join10(root, adapter.rulesRoot), paths);
4265
+ await walkFiles(root, join11(root, adapter.rulesRoot), paths);
3610
4266
  }
3611
- await walkHarnessEntries(root, join10(root, adapter.skillsRoot), paths);
4267
+ await walkHarnessEntries(root, join11(root, adapter.skillsRoot), paths);
3612
4268
  if (adapter.agentsRoot !== null) {
3613
- await walkHarnessEntries(root, join10(root, adapter.agentsRoot), paths);
4269
+ await walkHarnessEntries(root, join11(root, adapter.agentsRoot), paths);
3614
4270
  }
3615
4271
  }
3616
4272
  const result = {};
3617
4273
  for (const path of [...new Set(paths)].sort()) {
3618
- result[path] = await readFile9(join10(root, path), "utf8");
4274
+ result[path] = await readFile10(join11(root, path), "utf8");
3619
4275
  }
3620
4276
  return result;
3621
4277
  }
@@ -3624,14 +4280,14 @@ function proposalBaseline(baseline) {
3624
4280
  }
3625
4281
  async function readProject(root) {
3626
4282
  try {
3627
- 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")));
3628
4284
  } catch {
3629
4285
  throw new PushWorkflowError("project configuration is missing or invalid", 3, "PROJECT_CONFIG_INVALID");
3630
4286
  }
3631
4287
  }
3632
4288
  async function readOptionalJson(path) {
3633
4289
  try {
3634
- return JSON.parse(await readFile9(path, "utf8"));
4290
+ return JSON.parse(await readFile10(path, "utf8"));
3635
4291
  } catch (error) {
3636
4292
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
3637
4293
  return null;
@@ -3643,7 +4299,7 @@ async function clientIdFor(root, explicit) {
3643
4299
  if (explicit !== void 0) {
3644
4300
  return explicit;
3645
4301
  }
3646
- const path = join10(root, ".harness", "state", "local", "client.json");
4302
+ const path = join11(root, ".harness", "state", "local", "client.json");
3647
4303
  const existing = await readOptionalJson(path);
3648
4304
  if (typeof existing?.client_id === "string" && /^cli_[A-Za-z0-9_-]+$/.test(existing.client_id)) {
3649
4305
  return existing.client_id;
@@ -3732,6 +4388,35 @@ function assertPreviewAllowed(preview, skip) {
3732
4388
  }
3733
4389
  }
3734
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";
4391
+ var STALE_BASELINE_MESSAGE = "\u670D\u52A1\u7AEF artifact \u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5148\u6267\u884C npx hunter-harness update\uFF08\u51B2\u7A81\u53EF\u7528 update --resolve <path>=keep-local|accept-remote\uFF09\u518D\u63A8";
4392
+ function staleBaselineError(code, conflicts) {
4393
+ const conflictHint = conflicts !== void 0 && conflicts.length > 0 ? " \u51B2\u7A81\u6587\u4EF6\uFF1A" + conflicts.map((item2) => item2.path).join(", ") : "";
4394
+ return new PushWorkflowError(STALE_BASELINE_MESSAGE + conflictHint, 5, code);
4395
+ }
4396
+ async function syncToLatest(root, project, baseline, client) {
4397
+ const syncResult = await synchronizeArtifacts({
4398
+ projectRoot: root,
4399
+ project,
4400
+ client,
4401
+ requestId: uuidV7(),
4402
+ dryRun: false,
4403
+ conflictStrategy: "manual"
4404
+ }, baseline);
4405
+ if (syncResult.conflicts.length > 0) {
4406
+ throw staleBaselineError("PROJECT_VERSION_CONFLICT", syncResult.conflicts);
4407
+ }
4408
+ return await readBaseline(root);
4409
+ }
4410
+ async function autoRebaseIfServerAdvanced(root, project, baseline, client, remoteVersion) {
4411
+ if (remoteVersion === baseline.complete_project_version) {
4412
+ return baseline;
4413
+ }
4414
+ const updated = await syncToLatest(root, project, baseline, client);
4415
+ if (remoteVersion !== null && updated.complete_project_version !== remoteVersion) {
4416
+ throw staleBaselineError("PROJECT_VERSION_CONFLICT");
4417
+ }
4418
+ return updated;
4419
+ }
3735
4420
  function makePreview(baseline, files, confirmedProjectLocal, ignorePaths, deletedAt = (/* @__PURE__ */ new Date()).toISOString()) {
3736
4421
  const filteredFiles = {};
3737
4422
  for (const [path, content] of Object.entries(files)) {
@@ -3770,20 +4455,18 @@ async function bindProject(root, project, baseline, projectId) {
3770
4455
  return { project: nextProject, baseline: nextBaseline };
3771
4456
  }
3772
4457
  async function pushProject(options) {
3773
- const root = resolve5(options.projectRoot);
4458
+ const root = resolve6(options.projectRoot);
3774
4459
  let project = await readProject(root);
3775
4460
  let baseline = await readBaseline(root);
3776
4461
  const profile = parseHarnessProfile(project.project.profiles[0]);
3777
4462
  const installedPaths = profile === null ? /* @__PURE__ */ new Set() : new Set(await Promise.all(enabledHarnessAgents(project).map((agent) => managedBundleTargets(options.resourcesRoot, profile, agent))).then((targets) => targets.flatMap((target) => [...target])));
3778
4463
  let preview = makePreview(baseline, await managedFiles(root, project), options.confirmedProjectLocal ?? [], installedPaths);
3779
- const initialSkip = await resolveSensitiveScanSkip(preview, options);
3780
- if (initialSkip.cancelled === true) {
3781
- return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
3782
- }
3783
- let sensitiveScanSkip = initialSkip.skip;
3784
- let sensitiveScanSkipReason = initialSkip.reason;
3785
- assertPreviewAllowed(preview, sensitiveScanSkip);
3786
4464
  if (options.dryRun) {
4465
+ const drySkip = await resolveSensitiveScanSkip(preview, options);
4466
+ if (drySkip.cancelled === true) {
4467
+ return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
4468
+ }
4469
+ assertPreviewAllowed(preview, drySkip.skip);
3787
4470
  return { preview, proposalId: null, projectId: project.project.project_id };
3788
4471
  }
3789
4472
  const localCredentials = await readLocalCredentials(root);
@@ -3817,10 +4500,28 @@ async function pushProject(options) {
3817
4500
  if (parsedServerUrl.protocol !== "https:") {
3818
4501
  throw new PushWorkflowError("server_url must use HTTPS", 3, "SERVER_URL_INVALID");
3819
4502
  }
4503
+ const client = new HunterHarnessApiClient({
4504
+ serverUrl: parsedServerUrl.toString(),
4505
+ token,
4506
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch }
4507
+ });
4508
+ const boundAtStart = project.project.project_id;
4509
+ if (boundAtStart !== null) {
4510
+ const remote = await client.getProject(boundAtStart, uuidV7());
4511
+ baseline = await autoRebaseIfServerAdvanced(root, project, baseline, client, remote.latest_project_version);
4512
+ preview = makePreview(baseline, await managedFiles(root, project), options.confirmedProjectLocal ?? [], installedPaths);
4513
+ }
4514
+ const initialSkip = await resolveSensitiveScanSkip(preview, options);
4515
+ if (initialSkip.cancelled === true) {
4516
+ return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
4517
+ }
4518
+ let sensitiveScanSkip = initialSkip.skip;
4519
+ let sensitiveScanSkipReason = initialSkip.reason;
4520
+ assertPreviewAllowed(preview, sensitiveScanSkip);
3820
4521
  if (options.confirmProposal !== void 0 && !await options.confirmProposal(preview)) {
3821
4522
  return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
3822
4523
  }
3823
- const workflowPath = join10(root, ".harness", "state", "local", "push-workflow.json");
4524
+ const workflowPath = join11(root, ".harness", "state", "local", "push-workflow.json");
3824
4525
  const priorWorkflow = await readOptionalJson(workflowPath);
3825
4526
  const provisionalRequestId = priorWorkflow?.local_project_key === project.project.local_project_key ? priorWorkflow.request_id : uuidV7();
3826
4527
  const lock = await acquireProtocolLock(root, "push", { requestId: provisionalRequestId });
@@ -3831,11 +4532,6 @@ async function pushProject(options) {
3831
4532
  await atomicWriteJson(workflowPath, workflow);
3832
4533
  preview = makePreview(baseline, await managedFiles(root, project), options.confirmedProjectLocal ?? [], installedPaths, workflow.created_at);
3833
4534
  const requestId = workflow.request_id;
3834
- const client = new HunterHarnessApiClient({
3835
- serverUrl: parsedServerUrl.toString(),
3836
- token,
3837
- ...options.fetch === void 0 ? {} : { fetch: options.fetch }
3838
- });
3839
4535
  if (project.project.project_id === null) {
3840
4536
  const resolved = await client.resolveProject({
3841
4537
  schema_version: 1,
@@ -3916,7 +4612,9 @@ async function pushProject(options) {
3916
4612
  });
3917
4613
  }
3918
4614
  }
3919
- const finalized = await client.finalizeProposal(session.session_id, {
4615
+ let finalizeRetried = false;
4616
+ let finalized;
4617
+ const finalizeOnce = async () => client.finalizeProposal(session.session_id, {
3920
4618
  schema_version: 1,
3921
4619
  manifest_sha256: proposalManifestHash,
3922
4620
  base_artifact_id: baseline.latest_artifact_id ?? null,
@@ -3925,7 +4623,60 @@ async function pushProject(options) {
3925
4623
  ...sensitiveScanSkipReason === void 0 ? {} : { sensitive_scan_skip_reason: sensitiveScanSkipReason }
3926
4624
  } : {}
3927
4625
  }, requestId, workflow.finalize_idempotency_key);
3928
- await atomicWriteJson(join10(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
4626
+ try {
4627
+ finalized = await finalizeOnce();
4628
+ } catch (error) {
4629
+ if (error instanceof ApiError && (error.code === "STALE_PUSH" || error.code === "PROJECT_VERSION_CONFLICT") && !finalizeRetried) {
4630
+ finalizeRetried = true;
4631
+ baseline = await syncToLatest(root, project, baseline, client);
4632
+ workflow = resetSession(workflow, proposalManifestHash);
4633
+ await atomicWriteJson(workflowPath, workflow);
4634
+ session = await client.createProposalSession(projectId, {
4635
+ schema_version: 1,
4636
+ request_id: requestId,
4637
+ client_id: clientId,
4638
+ base_project_version: baseline.complete_project_version,
4639
+ base_manifest_hash: sha256Bytes(canonicalJson(baseline)),
4640
+ proposal_manifest: { files: preview.operations },
4641
+ artifact_manifest: { schema_version: 1, files: preview.operations }
4642
+ }, requestId, workflow.session_idempotency_key);
4643
+ workflow.session_id = session.session_id;
4644
+ workflow.session_expires_at = session.expires_at;
4645
+ workflow.max_chunk_bytes = session.max_chunk_bytes;
4646
+ await atomicWriteJson(workflowPath, workflow);
4647
+ try {
4648
+ finalized = await finalizeOnce();
4649
+ } catch (retryError) {
4650
+ if (retryError instanceof ApiError && (retryError.code === "STALE_PUSH" || retryError.code === "PROJECT_VERSION_CONFLICT")) {
4651
+ throw staleBaselineError(retryError.code);
4652
+ }
4653
+ throw retryError;
4654
+ }
4655
+ } else if (error instanceof ApiError && (error.code === "STALE_PUSH" || error.code === "PROJECT_VERSION_CONFLICT")) {
4656
+ throw staleBaselineError(error.code);
4657
+ } else {
4658
+ throw error;
4659
+ }
4660
+ }
4661
+ let pushWarning;
4662
+ if (finalized.artifact_id !== null) {
4663
+ try {
4664
+ const publishedManifest = artifactManifestSchema.parse(await client.getArtifactManifest(finalized.artifact_id, requestId));
4665
+ const advanced = await advanceBaselineFromArtifact({
4666
+ projectRoot: root,
4667
+ manifest: publishedManifest,
4668
+ requestId
4669
+ }, baseline);
4670
+ if (advanced.localChanged) {
4671
+ pushWarning = "LOCAL_CHANGED_DURING_PUSH";
4672
+ } else {
4673
+ baseline = advanced.baseline;
4674
+ }
4675
+ } catch {
4676
+ pushWarning = "BASELINE_ADVANCE_DEFERRED";
4677
+ }
4678
+ }
4679
+ await atomicWriteJson(join11(root, ".harness", "state", "local", "push-results", finalized.proposal_id + ".json"), {
3929
4680
  schema_version: 1,
3930
4681
  request_id: requestId,
3931
4682
  project_id: projectId,
@@ -3933,17 +4684,24 @@ async function pushProject(options) {
3933
4684
  status: finalized.status,
3934
4685
  artifact_id: finalized.artifact_id,
3935
4686
  operation_count: preview.operations.length,
4687
+ warning: pushWarning ?? null,
3936
4688
  recorded_at: (/* @__PURE__ */ new Date()).toISOString()
3937
4689
  });
3938
- await rm4(workflowPath, { force: true });
3939
- return { preview, proposalId: finalized.proposal_id, projectId, artifactId: finalized.artifact_id };
4690
+ await rm5(workflowPath, { force: true });
4691
+ return {
4692
+ preview,
4693
+ proposalId: finalized.proposal_id,
4694
+ projectId,
4695
+ artifactId: finalized.artifact_id,
4696
+ ...pushWarning === void 0 ? {} : { warning: pushWarning }
4697
+ };
3940
4698
  } catch (error) {
3941
4699
  if (error instanceof PushWorkflowError) {
3942
4700
  throw error;
3943
4701
  }
3944
4702
  if (error instanceof ApiError) {
3945
- if (error.code === "STALE_PUSH") {
3946
- throw new PushWorkflowError("\u670D\u52A1\u7AEF\u5DF2\u6709\u66F4\u65B0\u7684\u63A8\u9001\uFF0C\u8BF7\u5148 git pull \u540E\u6267\u884C npx hunter-harness update \u518D\u63A8", 5, "STALE_PUSH");
4703
+ if (error.code === "STALE_PUSH" || error.code === "PROJECT_VERSION_CONFLICT") {
4704
+ throw staleBaselineError(error.code);
3947
4705
  }
3948
4706
  if (error.code === "SENSITIVE_CONTENT_BLOCKED") {
3949
4707
  const details = error.details;
@@ -3963,8 +4721,8 @@ async function pushProject(options) {
3963
4721
  }
3964
4722
 
3965
4723
  // ../core/dist/state/cleanup.js
3966
- import { readFile as readFile10, readdir as readdir5, rm as rm5 } from "node:fs/promises";
3967
- import { join as join11 } from "node:path";
4724
+ import { readFile as readFile11, readdir as readdir5, rm as rm6 } from "node:fs/promises";
4725
+ import { join as join12 } from "node:path";
3968
4726
  function isSafeEntryName(name) {
3969
4727
  return name.length > 0 && !name.includes("/") && !name.includes("\\") && name !== "." && name !== ".." && !name.includes("\0");
3970
4728
  }
@@ -3988,7 +4746,7 @@ async function cleanupProject(options) {
3988
4746
  continue;
3989
4747
  let journal;
3990
4748
  try {
3991
- journal = JSON.parse(await readFile10(join11(layout.transactions, name, "journal.json"), "utf8"));
4749
+ journal = JSON.parse(await readFile11(join12(layout.transactions, name, "journal.json"), "utf8"));
3992
4750
  } catch {
3993
4751
  continue;
3994
4752
  }
@@ -4006,7 +4764,7 @@ async function cleanupProject(options) {
4006
4764
  continue;
4007
4765
  pruned.push(entry.id);
4008
4766
  if (!options.dryRun) {
4009
- await rm5(join11(layout.transactions, entry.id), { recursive: true, force: true });
4767
+ await rm6(join12(layout.transactions, entry.id), { recursive: true, force: true });
4010
4768
  }
4011
4769
  }
4012
4770
  }
@@ -4015,7 +4773,7 @@ async function cleanupProject(options) {
4015
4773
  continue;
4016
4774
  removedCache.push(name);
4017
4775
  if (!options.dryRun) {
4018
- await rm5(join11(layout.serverArtifacts, name), { recursive: true, force: true });
4776
+ await rm6(join12(layout.serverArtifacts, name), { recursive: true, force: true });
4019
4777
  }
4020
4778
  }
4021
4779
  return {
@@ -4069,10 +4827,10 @@ var AGENT_DESCRIPTORS = {
4069
4827
  var INSTALLABLE_AGENTS = Object.keys(AGENT_DESCRIPTORS).filter((agent) => AGENT_DESCRIPTORS[agent]?.installable === true);
4070
4828
 
4071
4829
  // ../core/dist/transaction/recovery.js
4072
- import { readFile as readFile11, readdir as readdir6, rm as rm6, stat as stat3 } from "node:fs/promises";
4073
- import { join as join12 } from "node:path";
4830
+ import { readFile as readFile12, readdir as readdir6, rm as rm7, stat as stat3 } from "node:fs/promises";
4831
+ import { join as join13 } from "node:path";
4074
4832
  async function recoverTransaction(projectRoot, transactionId) {
4075
- const journal = JSON.parse(await readFile11(join12(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
4833
+ const journal = JSON.parse(await readFile12(join13(stateLayout(projectRoot).transactions, transactionId, "journal.json"), "utf8"));
4076
4834
  if (journal.state === "committed") {
4077
4835
  return { transactionId, status: "committed" };
4078
4836
  }
@@ -4099,7 +4857,7 @@ async function listTransactions(projectRoot) {
4099
4857
  const transactions = [];
4100
4858
  for (const transactionId of names) {
4101
4859
  try {
4102
- const journal = JSON.parse(await readFile11(join12(root, transactionId, "journal.json"), "utf8"));
4860
+ const journal = JSON.parse(await readFile12(join13(root, transactionId, "journal.json"), "utf8"));
4103
4861
  transactions.push({
4104
4862
  transactionId,
4105
4863
  kind: journal.kind,
@@ -4114,7 +4872,7 @@ async function listTransactions(projectRoot) {
4114
4872
  async function pendingTransactions(projectRoot) {
4115
4873
  return (await listTransactions(projectRoot)).filter((item2) => RECOVERY_STATES.has(item2.state));
4116
4874
  }
4117
- async function pathExists(path) {
4875
+ async function pathExists2(path) {
4118
4876
  try {
4119
4877
  await stat3(path);
4120
4878
  return true;
@@ -4130,12 +4888,12 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
4130
4888
  if (latest === void 0) {
4131
4889
  throw new Error("no committed update transaction is available for rollback");
4132
4890
  }
4133
- const transactionRoot = join12(stateLayout(projectRoot).transactions, latest.transactionId);
4134
- const journal = JSON.parse(await readFile11(join12(transactionRoot, "journal.json"), "utf8"));
4135
- const after = JSON.parse(await readFile11(join12(transactionRoot, "after", "manifest.json"), "utf8"));
4891
+ const transactionRoot = join13(stateLayout(projectRoot).transactions, latest.transactionId);
4892
+ const journal = JSON.parse(await readFile12(join13(transactionRoot, "journal.json"), "utf8"));
4893
+ const after = JSON.parse(await readFile12(join13(transactionRoot, "after", "manifest.json"), "utf8"));
4136
4894
  for (const entry of after) {
4137
- const target = join12(projectRoot, entry.path);
4138
- const exists6 = await pathExists(target);
4895
+ const target = join13(projectRoot, entry.path);
4896
+ const exists6 = await pathExists2(target);
4139
4897
  if (exists6 !== entry.exists || exists6 && await sha256File(target) !== entry.hash) {
4140
4898
  throw new Error("cannot rollback dirty path: " + entry.path);
4141
4899
  }
@@ -4147,10 +4905,10 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
4147
4905
  continue;
4148
4906
  }
4149
4907
  seen.add(snapshot.path);
4150
- const target = join12(projectRoot, snapshot.path);
4151
- const exists6 = await pathExists(target);
4908
+ const target = join13(projectRoot, snapshot.path);
4909
+ const exists6 = await pathExists2(target);
4152
4910
  if (snapshot.existed && snapshot.snapshot_name !== null) {
4153
- const content = await readFile11(join12(transactionRoot, "before", snapshot.snapshot_name));
4911
+ const content = await readFile12(join13(transactionRoot, "before", snapshot.snapshot_name));
4154
4912
  operations.push({
4155
4913
  operation: exists6 ? "modify" : "add",
4156
4914
  path: snapshot.path,
@@ -4179,7 +4937,7 @@ async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Dat
4179
4937
  if (!removable) {
4180
4938
  continue;
4181
4939
  }
4182
- await rm6(join12(stateLayout(projectRoot).transactions, item2.transactionId), {
4940
+ await rm7(join13(stateLayout(projectRoot).transactions, item2.transactionId), {
4183
4941
  recursive: true,
4184
4942
  force: true
4185
4943
  });
@@ -4188,31 +4946,9 @@ async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Dat
4188
4946
  return removed;
4189
4947
  }
4190
4948
 
4191
- // ../core/dist/update/conflicts.js
4192
- function operationTargetPath(operation) {
4193
- return operation.operation === "rename" ? operation.to_path : operation.path;
4194
- }
4195
- function operationSourcePath(operation) {
4196
- return operation.operation === "rename" ? operation.from_path : operation.path;
4197
- }
4198
- function operationAlreadyApplied(operation, baseline, projectVersion) {
4199
- const target = baseline.files[operationTargetPath(operation)];
4200
- if (operation.operation === "delete") {
4201
- return target?.deleted === true && target.last_applied_version === projectVersion;
4202
- }
4203
- return target?.deleted === false && target?.last_applied_version === projectVersion && target.baseline_hash === operation.content_sha256;
4204
- }
4205
- function managedBlockDirty(currentContent, managedBlockHash) {
4206
- if (managedBlockHash === void 0) {
4207
- return true;
4208
- }
4209
- const block = extractManagedBlock(currentContent);
4210
- return block === null || sha256Bytes(block) !== managedBlockHash;
4211
- }
4212
-
4213
4949
  // ../core/dist/update/update.js
4214
- import { lstat as lstat3, readFile as readFile12, rm as rm7 } from "node:fs/promises";
4215
- import { join as join13, resolve as resolve6 } from "node:path";
4950
+ import { readFile as readFile13 } from "node:fs/promises";
4951
+ import { join as join14, resolve as resolve7 } from "node:path";
4216
4952
  import { parse as parseYaml8 } from "yaml";
4217
4953
  var UpdateWorkflowError = class extends Error {
4218
4954
  exitCode;
@@ -4224,107 +4960,31 @@ var UpdateWorkflowError = class extends Error {
4224
4960
  this.code = code;
4225
4961
  }
4226
4962
  };
4227
- async function pathExists2(path) {
4228
- try {
4229
- await lstat3(path);
4230
- return true;
4231
- } catch (error) {
4232
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
4233
- return false;
4234
- }
4235
- throw error;
4236
- }
4237
- }
4238
- async function optionalContent(path) {
4239
- return await pathExists2(path) ? readFile12(path, "utf8") : null;
4240
- }
4241
- function manifestPayloadHash(manifest) {
4242
- const payload = { ...manifest };
4243
- delete payload.manifest_sha256;
4244
- return sha256Bytes(canonicalJson(payload));
4245
- }
4246
- function expectedBase(operation) {
4247
- return operation.operation === "add" ? null : operation.base_content_sha256;
4248
- }
4249
- function contentHash(operation) {
4250
- return operation.operation === "delete" ? null : operation.content_sha256;
4251
- }
4252
- async function loadBlob(root, client, artifactId, operation, requestId, dryRun) {
4253
- if (operation.operation === "delete") {
4254
- return null;
4255
- }
4256
- const hash = operation.content_sha256;
4257
- const cacheRoot = join13(root, ".harness", "cache", "server-artifacts", artifactId);
4258
- const cachePath = join13(cacheRoot, "blobs", hash.replace(":", "_"));
4259
- if (await pathExists2(cachePath) && await sha256File(cachePath) === hash) {
4260
- return readFile12(cachePath, "utf8");
4261
- }
4262
- const bytes = await client.downloadArtifactBlob(artifactId, hash, requestId);
4263
- if (bytes.byteLength !== operation.size_bytes || sha256Bytes(bytes) !== hash) {
4264
- await rm7(cachePath, { force: true });
4265
- throw new UpdateWorkflowError("artifact blob size or hash mismatch", 4, "ARTIFACT_HASH_MISMATCH");
4266
- }
4267
- if (!dryRun) {
4268
- await atomicWriteFile(cachePath, bytes);
4269
- }
4270
- return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
4271
- }
4272
- function nextBaselineEntry(operation, finalContent, projectVersion) {
4273
- const block = finalContent === null ? null : extractManagedBlock(finalContent);
4274
- return {
4275
- baseline_hash: contentHash(operation),
4276
- local_hash_at_apply: finalContent === null ? null : sha256Bytes(finalContent),
4277
- file_kind: operation.file_kind,
4278
- last_applied_version: projectVersion,
4279
- deleted: operation.operation === "delete",
4280
- ...block === null ? {} : { managed_block_hash: sha256Bytes(block) }
4281
- };
4282
- }
4283
- function transactionOperation(item2) {
4284
- if (item2.equivalent) {
4285
- return null;
4286
- }
4287
- const operation = item2.operation;
4288
- const target = operationTargetPath(operation);
4289
- if (operation.operation === "delete") {
4290
- return item2.finalContent === null ? { operation: "delete", path: target } : { operation: "modify", path: target, content: item2.finalContent };
4291
- }
4292
- if (operation.operation === "rename") {
4293
- return {
4294
- operation: "rename",
4295
- from_path: operation.from_path,
4296
- to_path: operation.to_path,
4297
- content: item2.finalContent ?? item2.content ?? ""
4298
- };
4299
- }
4300
- return {
4301
- operation: "modify",
4302
- path: target,
4303
- content: item2.finalContent ?? item2.content ?? ""
4304
- };
4305
- }
4306
4963
  async function updateProject(options) {
4307
- const root = resolve6(options.projectRoot);
4964
+ const root = resolve7(options.projectRoot);
4308
4965
  let project;
4309
4966
  try {
4310
- project = projectConfigSchema.parse(parseYaml8(await readFile12(join13(root, ".harness", "project.yaml"), "utf8")));
4967
+ project = projectConfigSchema.parse(parseYaml8(await readFile13(join14(root, ".harness", "project.yaml"), "utf8")));
4311
4968
  } catch {
4312
4969
  throw new UpdateWorkflowError("project configuration is invalid", 3, "PROJECT_CONFIG_INVALID");
4313
4970
  }
4314
4971
  if (project.project.project_id === null) {
4315
4972
  throw new UpdateWorkflowError("project is not bound to a server", 3, "PROJECT_NOT_BOUND");
4316
4973
  }
4317
- const serverUrl = options.serverUrl ?? project.server.url;
4318
4974
  const tokenEnv = options.tokenEnv ?? project.server.token_env;
4319
- if (serverUrl === null || serverUrl === void 0) {
4320
- throw new UpdateWorkflowError("server_url is required", 3, "SERVER_URL_REQUIRED");
4321
- }
4322
4975
  if (!/^[A-Z_][A-Z0-9_]*$/.test(tokenEnv)) {
4323
4976
  throw new UpdateWorkflowError("token_env is invalid", 3, "TOKEN_ENV_INVALID");
4324
4977
  }
4325
- const token = options.env[tokenEnv];
4326
- if (token === void 0 || token.trim() === "") {
4327
- throw new UpdateWorkflowError("API token environment variable is unset", 8, "TOKEN_INVALID");
4978
+ const local = await readLocalCredentials(root);
4979
+ const serverUrl = options.serverUrl ?? local?.server_url ?? project.server.url;
4980
+ if (serverUrl === null || serverUrl === void 0) {
4981
+ throw new UpdateWorkflowError("server_url is required", 3, "SERVER_URL_REQUIRED");
4982
+ }
4983
+ const envToken = options.env[tokenEnv]?.trim();
4984
+ const localToken = local?.token?.trim();
4985
+ const token = envToken !== void 0 && envToken !== "" ? envToken : localToken !== void 0 && localToken !== "" ? localToken : void 0;
4986
+ if (token === void 0) {
4987
+ throw new UpdateWorkflowError(`API token \u672A\u914D\u7F6E\uFF1A\u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF ${tokenEnv}\uFF0C\u6216\u5728 .harness/credentials.local.yaml \u914D\u7F6E token\uFF08\u53EF\u901A\u8FC7 npx hunter-harness push \u5F15\u5BFC\u5199\u5165\uFF09`, 8, "TOKEN_INVALID");
4328
4988
  }
4329
4989
  const requestId = uuidV7();
4330
4990
  const baseline = await readBaseline(root);
@@ -4343,183 +5003,18 @@ async function updateProject(options) {
4343
5003
  ...options.fetch === void 0 ? {} : { fetch: options.fetch }
4344
5004
  });
4345
5005
  try {
4346
- const discovery = await client.getUpdateManifest(project.project.project_id, {
4347
- base_project_version: baseline.complete_project_version,
4348
- base_manifest_hash: sha256Bytes(canonicalJson(baseline)),
4349
- adapter: project.adapters.enabled[0] ?? "claude-code",
4350
- profile: project.project.profiles[0] ?? "general"
4351
- }, requestId);
4352
- if (!discovery.delta_available || discovery.artifact_id === null) {
4353
- return {
4354
- requestId,
4355
- projectId: project.project.project_id,
4356
- artifactId: null,
4357
- observedProjectVersion: discovery.observed_project_version,
4358
- operations: [],
4359
- applied: [],
4360
- skipped: [],
4361
- transactionId: null,
4362
- dryRun: options.dryRun
4363
- };
4364
- }
4365
- const manifest = artifactManifestSchema.parse(await client.getArtifactManifest(discovery.artifact_id, requestId));
4366
- if (manifest.artifact_id !== discovery.artifact_id || manifest.project_id !== project.project.project_id || manifest.project_version === null || manifestPayloadHash(manifest) !== manifest.manifest_sha256) {
4367
- throw new UpdateWorkflowError("artifact manifest integrity check failed", 4, "ARTIFACT_HASH_MISMATCH");
4368
- }
4369
- const blobs = /* @__PURE__ */ new Map();
4370
- for (const operation of manifest.files) {
4371
- const policy = classifyFile(operationTargetPath(operation));
4372
- if (decideUpdate(policy, false).apply) {
4373
- blobs.set(operation, await loadBlob(root, client, manifest.artifact_id, operation, requestId, options.dryRun));
4374
- }
4375
- }
4376
- const prepared = [];
4377
- const skipped = [];
4378
- for (const operation of manifest.files) {
4379
- if (operationAlreadyApplied(operation, baseline, manifest.project_version)) {
4380
- continue;
4381
- }
4382
- const source = operationSourcePath(operation);
4383
- const target = operationTargetPath(operation);
4384
- const policy = classifyFile(target);
4385
- const staticDecision = decideUpdate(policy, false);
4386
- if (!staticDecision.apply) {
4387
- skipped.push({ path: target, operation: operation.operation, reason: staticDecision.reason });
4388
- continue;
4389
- }
4390
- const previous = baseline.files[source];
4391
- if (expectedBase(operation) !== (previous?.baseline_hash ?? null)) {
4392
- skipped.push({ path: target, operation: operation.operation, reason: "baseline-diverged" });
4393
- continue;
4394
- }
4395
- const sourceContent = await optionalContent(join13(root, source));
4396
- const targetContent = target === source ? sourceContent : await optionalContent(join13(root, target));
4397
- const incoming = blobs.get(operation) ?? null;
4398
- const incomingHash = contentHash(operation);
4399
- let equivalent = incomingHash !== null && targetContent !== null && sha256Bytes(targetContent) === incomingHash;
4400
- let dirty = false;
4401
- if (operation.operation === "add") {
4402
- if (targetContent !== null && !equivalent) {
4403
- if (policy.update_policy === "managed-block-only" && incoming !== null) {
4404
- const incomingBlock = extractManagedBlock(incoming) ?? incoming.trim();
4405
- equivalent = extractManagedBlock(targetContent) === incomingBlock;
4406
- dirty = !equivalent;
4407
- } else {
4408
- dirty = true;
4409
- }
4410
- }
4411
- } else if (sourceContent === null) {
4412
- dirty = operation.operation !== "delete";
4413
- } else if (policy.update_policy === "managed-block-only") {
4414
- dirty = previous?.managed_block_hash === void 0 ? (previous?.local_hash_at_apply ?? previous?.baseline_hash) !== sha256Bytes(sourceContent) : managedBlockDirty(sourceContent, previous.managed_block_hash);
4415
- } else {
4416
- dirty = (previous?.local_hash_at_apply ?? previous?.baseline_hash) !== sha256Bytes(sourceContent);
4417
- }
4418
- if (operation.operation === "rename" && targetContent !== null && !equivalent) {
4419
- dirty = true;
4420
- }
4421
- if (dirty) {
4422
- skipped.push({ path: target, operation: operation.operation, reason: "local-dirty" });
4423
- continue;
4424
- }
4425
- let finalContent = incoming;
4426
- if (policy.update_policy === "managed-block-only") {
4427
- if (operation.operation === "delete") {
4428
- finalContent = sourceContent === null ? null : removeManagedBlock(sourceContent);
4429
- equivalent = finalContent === sourceContent;
4430
- } else if (incoming !== null) {
4431
- const incomingBlock = extractManagedBlock(incoming) ?? incoming.trim();
4432
- const incomingId = extractSingleManagedBlockById(incoming)?.id;
4433
- const blockId = operation.operation === "add" || operation.operation === "modify" ? operation.block_id ?? incomingId : void 0;
4434
- finalContent = blockId !== void 0 ? upsertManagedBlockById(targetContent ?? "", blockId, incomingBlock) : upsertManagedBlock(targetContent ?? "", incomingBlock);
4435
- equivalent = finalContent === targetContent;
4436
- }
4437
- } else if (operation.operation === "delete" && sourceContent === null) {
4438
- equivalent = true;
4439
- }
4440
- prepared.push({ operation, content: incoming, finalContent, equivalent });
4441
- }
4442
- const applied = prepared.map((item2) => operationTargetPath(item2.operation));
4443
- if (options.dryRun) {
4444
- return {
4445
- requestId,
4446
- projectId: project.project.project_id,
4447
- artifactId: manifest.artifact_id,
4448
- observedProjectVersion: manifest.project_version,
4449
- operations: manifest.files,
4450
- applied,
4451
- skipped,
4452
- transactionId: null,
4453
- dryRun: true
4454
- };
4455
- }
4456
- await atomicWriteJson(join13(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
4457
- const lock = await acquireProtocolLock(root, "update", { requestId });
4458
- try {
4459
- const nextBaseline = baselineManifestSchema.parse(structuredClone(baseline));
4460
- for (const item2 of prepared) {
4461
- const target = operationTargetPath(item2.operation);
4462
- nextBaseline.files[target] = nextBaselineEntry(item2.operation, item2.finalContent, manifest.project_version);
4463
- if (item2.operation.operation === "rename") {
4464
- nextBaseline.files[item2.operation.from_path] = {
4465
- baseline_hash: null,
4466
- local_hash_at_apply: null,
4467
- file_kind: item2.operation.file_kind,
4468
- last_applied_version: manifest.project_version,
4469
- deleted: true
4470
- };
4471
- }
4472
- }
4473
- if (skipped.length === 0) {
4474
- nextBaseline.complete_project_version = manifest.project_version;
4475
- nextBaseline.artifact_manifest_hash = manifest.manifest_sha256;
4476
- nextBaseline.latest_artifact_id = manifest.artifact_id;
4477
- }
4478
- const transactionId = "tx_update_" + Date.now() + "_" + uuidV7();
4479
- const reportPath = ".harness/reports/update-" + requestId + ".json";
4480
- const report = {
4481
- schema_version: 1,
4482
- request_id: requestId,
4483
- artifact_id: manifest.artifact_id,
4484
- observed_project_version: manifest.project_version,
4485
- status: skipped.length === 0 ? "applied" : "partial_due_to_dirty",
4486
- applied,
4487
- skipped,
4488
- transaction_id: transactionId
4489
- };
4490
- const operations = prepared.map((item2) => transactionOperation(item2)).filter((item2) => item2 !== null);
4491
- operations.push({
4492
- operation: "modify",
4493
- path: ".harness/state/baseline/manifest.json",
4494
- content: JSON.stringify(nextBaseline, null, 2) + "\n"
4495
- }, {
4496
- operation: "add",
4497
- path: reportPath,
4498
- content: JSON.stringify(report, null, 2) + "\n"
4499
- }, {
4500
- operation: "modify",
4501
- path: ".harness/state/local/last-update.json",
4502
- content: JSON.stringify(report, null, 2) + "\n"
4503
- });
4504
- await runTransaction(root, operations, {
4505
- id: transactionId,
4506
- kind: "update",
4507
- ...options.transactionOptions ?? {}
4508
- });
4509
- return {
4510
- requestId,
4511
- projectId: project.project.project_id,
4512
- artifactId: manifest.artifact_id,
4513
- observedProjectVersion: manifest.project_version,
4514
- operations: manifest.files,
4515
- applied,
4516
- skipped,
4517
- transactionId,
4518
- dryRun: false
4519
- };
4520
- } finally {
4521
- await lock.release();
4522
- }
5006
+ const result = await synchronizeArtifacts({
5007
+ projectRoot: root,
5008
+ project,
5009
+ client,
5010
+ requestId,
5011
+ dryRun: options.dryRun,
5012
+ ...options.conflictStrategy === void 0 ? {} : { conflictStrategy: options.conflictStrategy },
5013
+ ...options.resolveOverrides === void 0 ? {} : { resolveOverrides: options.resolveOverrides },
5014
+ ...options.confirmConflictStrategy === void 0 ? {} : { confirmConflictStrategy: options.confirmConflictStrategy },
5015
+ ...options.transactionOptions === void 0 ? {} : { transactionOptions: options.transactionOptions }
5016
+ }, baseline);
5017
+ return result;
4523
5018
  } catch (error) {
4524
5019
  if (error instanceof UpdateWorkflowError) {
4525
5020
  throw error;
@@ -4527,16 +5022,25 @@ async function updateProject(options) {
4527
5022
  if (error instanceof ApiError) {
4528
5023
  throw new UpdateWorkflowError(error.message, error.status === 401 || error.status === 403 ? 8 : error.status === 409 ? 5 : 4, error.code);
4529
5024
  }
5025
+ if (error instanceof Error && error.message === "ARTIFACT_HASH_MISMATCH") {
5026
+ throw new UpdateWorkflowError("artifact blob size or hash mismatch", 4, "ARTIFACT_HASH_MISMATCH");
5027
+ }
4530
5028
  if (error instanceof Error && error.name === "ZodError") {
4531
5029
  throw new UpdateWorkflowError("artifact schema validation failed", 7, "SCHEMA_VALIDATION_FAILED");
4532
5030
  }
5031
+ if (error instanceof Error && error.message.startsWith("DUPLICATE_ARTIFACT_ID")) {
5032
+ throw new UpdateWorkflowError(error.message, 4, "DUPLICATE_ARTIFACT_ID");
5033
+ }
5034
+ if (error instanceof Error && error.message === "MAX_SYNC_ARTIFACT_ITERATIONS_EXCEEDED") {
5035
+ throw new UpdateWorkflowError(error.message, 4, "SYNC_ITERATION_LIMIT");
5036
+ }
4533
5037
  throw new UpdateWorkflowError(error instanceof Error ? error.message : "update failed", 4, "NETWORK_OR_SERVER_ERROR");
4534
5038
  }
4535
5039
  }
4536
5040
 
4537
5041
  // src/config/init-config.ts
4538
- import { isAbsolute as isAbsolute2, join as join14 } from "node:path";
4539
- import { readFile as readFile13 } from "node:fs/promises";
5042
+ import { isAbsolute as isAbsolute2, join as join15 } from "node:path";
5043
+ import { readFile as readFile14 } from "node:fs/promises";
4540
5044
  var InitConfigurationError = class extends Error {
4541
5045
  exitCode;
4542
5046
  code;
@@ -4569,13 +5073,15 @@ function normalizeProfile(value) {
4569
5073
  if (value === "2" || value === "java") return "java";
4570
5074
  throw new InitConfigurationError("\u914D\u7F6E\u7C7B\u578B\u5FC5\u987B\u4E3A general \u6216 java");
4571
5075
  }
5076
+ var ALL_AGENT_TOKENS = /* @__PURE__ */ new Set(["all", "5"]);
4572
5077
  function parseAgentsInput(raw) {
4573
5078
  const trimmed = raw.trim();
4574
5079
  if (trimmed === "") return ["claude-code"];
4575
- if (trimmed === "all") return [...HARNESS_AGENT_ORDER];
5080
+ if (ALL_AGENT_TOKENS.has(trimmed)) return [...HARNESS_AGENT_ORDER];
4576
5081
  const agents = [];
4577
5082
  for (const token of trimmed.split(",")) {
4578
5083
  const value = token.trim();
5084
+ if (ALL_AGENT_TOKENS.has(value)) return [...HARNESS_AGENT_ORDER];
4579
5085
  const byIndex = AGENT_BY_INDEX[value];
4580
5086
  if (byIndex !== void 0) {
4581
5087
  agents.push(byIndex);
@@ -4617,9 +5123,9 @@ function hasOwn(record, key) {
4617
5123
  async function resolveInitConfig(cwd, flags, prompts = {}, warnings = []) {
4618
5124
  let fileConfig = {};
4619
5125
  if (flags.config !== void 0) {
4620
- const path = isAbsolute2(flags.config) ? flags.config : join14(cwd, flags.config);
5126
+ const path = isAbsolute2(flags.config) ? flags.config : join15(cwd, flags.config);
4621
5127
  try {
4622
- fileConfig = JSON.parse(await readFile13(path, "utf8"));
5128
+ fileConfig = JSON.parse(await readFile14(path, "utf8"));
4623
5129
  } catch (error) {
4624
5130
  throw new InitConfigurationError(
4625
5131
  "unable to read init config: " + (error instanceof Error ? error.message : String(error)),
@@ -4701,8 +5207,8 @@ function serializeCliResult(result) {
4701
5207
  }
4702
5208
 
4703
5209
  // src/config/codebuddy-setup.ts
4704
- import { mkdir as mkdir4, readFile as readFile14, readdir as readdir7, stat as stat4, writeFile as writeFile5 } from "node:fs/promises";
4705
- import { basename as basename2, dirname as dirname4, extname, join as join15 } from "node:path";
5210
+ import { mkdir as mkdir4, readFile as readFile15, readdir as readdir7, stat as stat4, writeFile as writeFile5 } from "node:fs/promises";
5211
+ import { basename as basename2, dirname as dirname4, extname, join as join16 } from "node:path";
4706
5212
  var MANAGED_RULE_NAMES = /* @__PURE__ */ new Set([
4707
5213
  "harness-general.md",
4708
5214
  "harness-general.mdc",
@@ -4721,7 +5227,7 @@ async function exists4(path) {
4721
5227
  }
4722
5228
  async function readJsonObject(path) {
4723
5229
  try {
4724
- const parsed = JSON.parse(await readFile14(path, "utf8"));
5230
+ const parsed = JSON.parse(await readFile15(path, "utf8"));
4725
5231
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
4726
5232
  } catch (error) {
4727
5233
  if (error instanceof Error && "code" in error && error.code === "ENOENT") return {};
@@ -4729,27 +5235,27 @@ async function readJsonObject(path) {
4729
5235
  }
4730
5236
  }
4731
5237
  async function inspectCodeBuddySetup(projectRoot) {
4732
- const rulesRoot = join15(projectRoot, ".claude", "rules");
5238
+ const rulesRoot = join16(projectRoot, ".claude", "rules");
4733
5239
  let claudeRules = [];
4734
5240
  try {
4735
5241
  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();
4736
5242
  } catch (error) {
4737
5243
  if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
4738
5244
  }
4739
- const mcp = await readJsonObject(join15(projectRoot, ".mcp.json"));
5245
+ const mcp = await readJsonObject(join16(projectRoot, ".mcp.json"));
4740
5246
  const servers = mcp?.mcpServers;
4741
5247
  const configured = servers !== null && typeof servers === "object" && !Array.isArray(servers) && Object.prototype.hasOwnProperty.call(servers, "codegraph");
4742
5248
  return {
4743
5249
  claudeRules,
4744
- hasCodeGraphIndex: await exists4(join15(projectRoot, ".codegraph")),
5250
+ hasCodeGraphIndex: await exists4(join16(projectRoot, ".codegraph")),
4745
5251
  codeGraphConfigured: configured
4746
5252
  };
4747
5253
  }
4748
5254
  function destinationTargets(root, surface, name) {
4749
5255
  const stem = basename2(name, extname(name));
4750
5256
  const targets = [];
4751
- if (surface !== "cli") targets.push(join15(root, ".codebuddy", ".rules", `${stem}.mdc`));
4752
- if (surface !== "ide") targets.push(join15(root, ".codebuddy", "rules", `${stem}.md`));
5257
+ if (surface !== "cli") targets.push(join16(root, ".codebuddy", ".rules", `${stem}.mdc`));
5258
+ if (surface !== "ide") targets.push(join16(root, ".codebuddy", "rules", `${stem}.md`));
4753
5259
  return targets;
4754
5260
  }
4755
5261
  async function applyCodeBuddySetup(options) {
@@ -4763,8 +5269,8 @@ async function applyCodeBuddySetup(options) {
4763
5269
  if (options.syncClaudeRules) {
4764
5270
  const plan = await inspectCodeBuddySetup(options.projectRoot);
4765
5271
  for (const name of plan.claudeRules) {
4766
- const source = join15(options.projectRoot, ".claude", "rules", name);
4767
- const content = await readFile14(source, "utf8");
5272
+ const source = join16(options.projectRoot, ".claude", "rules", name);
5273
+ const content = await readFile15(source, "utf8");
4768
5274
  if (SENSITIVE_ASSIGNMENT.test(content) || PRIVATE_KEY_BLOCK.test(content)) {
4769
5275
  result.skippedSensitive.push(name);
4770
5276
  continue;
@@ -4781,7 +5287,7 @@ async function applyCodeBuddySetup(options) {
4781
5287
  }
4782
5288
  }
4783
5289
  if (options.configureCodeGraph) {
4784
- const path = join15(options.projectRoot, ".mcp.json");
5290
+ const path = join16(options.projectRoot, ".mcp.json");
4785
5291
  const current = await readJsonObject(path);
4786
5292
  if (current === null) {
4787
5293
  result.warnings.push(".mcp.json \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u5DF2\u4FDD\u7559\u539F\u6587\u4EF6\u5E76\u8DF3\u8FC7 CodeGraph MCP \u914D\u7F6E");
@@ -4805,13 +5311,13 @@ async function applyCodeBuddySetup(options) {
4805
5311
  }
4806
5312
 
4807
5313
  // src/commands/refresh.ts
4808
- import { readFile as readFile15 } from "node:fs/promises";
4809
- import { join as join16 } from "node:path";
5314
+ import { readFile as readFile16 } from "node:fs/promises";
5315
+ import { join as join17 } from "node:path";
4810
5316
  import { parse as parseYaml9 } from "yaml";
4811
5317
  async function detectProject(root) {
4812
5318
  let content;
4813
5319
  try {
4814
- content = await readFile15(join16(root, ".harness", "project.yaml"), "utf8");
5320
+ content = await readFile16(join17(root, ".harness", "project.yaml"), "utf8");
4815
5321
  } catch (error) {
4816
5322
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
4817
5323
  return { status: "absent" };
@@ -5018,6 +5524,14 @@ var AGENT_LABELS = {
5018
5524
  cursor: "Cursor",
5019
5525
  codebuddy: "CodeBuddy"
5020
5526
  };
5527
+ function agentMenuLines(installedProfiles) {
5528
+ const lines = HARNESS_AGENT_ORDER.map((agent, index) => {
5529
+ const profile = installedProfiles?.[agent];
5530
+ const suffix = profile === void 0 ? "" : `\uFF08\u5DF2\u5B89\u88C5\uFF1A${profile}\uFF09`;
5531
+ return ` ${index + 1}. ${AGENT_LABELS[agent]}${suffix}`;
5532
+ }).join("\n");
5533
+ return lines + "\n 5. \u5168\u90E8";
5534
+ }
5021
5535
  async function configureCodeBuddyExtras(agents, surface, options, dependencies) {
5022
5536
  if (!agents.includes("codebuddy")) return;
5023
5537
  const plan = await inspectCodeBuddySetup(dependencies.cwd);
@@ -5067,7 +5581,7 @@ async function runFirstInstall(options, dependencies) {
5067
5581
  options,
5068
5582
  options.nonInteractive === true ? {} : {
5069
5583
  agents: () => dependencies.prompt(
5070
- "\u8BF7\u9009\u62E9\u76EE\u6807 Agent\uFF08\u53EF\u591A\u9009\uFF0C\u4F7F\u7528\u9017\u53F7\u5206\u9694\uFF09\n 1. Claude Code\n 2. Codex\n 3. Cursor\n 4. CodeBuddy\n\u8BF7\u8F93\u5165\u7F16\u53F7 [1]: "
5584
+ "\u8BF7\u9009\u62E9\u76EE\u6807 Agent\uFF08\u53EF\u591A\u9009\uFF0C\u4F7F\u7528\u9017\u53F7\u5206\u9694\uFF09\n" + agentMenuLines() + "\n\u8BF7\u8F93\u5165\u7F16\u53F7 [1]: "
5071
5585
  ).then((answer) => answer.trim()),
5072
5586
  profile: () => dependencies.prompt(
5073
5587
  "\u8BF7\u9009\u62E9 Harness \u7C7B\u578B\uFF1A\n1. \u901A\u7528\uFF08\u9ED8\u8BA4\uFF09\n2. Java\n\u8BF7\u8F93\u5165 1 \u6216 2 [1]: "
@@ -5165,10 +5679,7 @@ async function runExistingProject(options, dependencies, currentProfile, current
5165
5679
  `Hunter Harness \u5F53\u524D\u914D\u7F6E\uFF1A
5166
5680
  ${currentLines}
5167
5681
  \u8BF7\u9009\u62E9\u672C\u6B21\u8981\u65B0\u589E\u6216\u5237\u65B0\u7684\u5DE5\u5177\uFF08\u53EF\u591A\u9009\uFF0C\u9017\u53F7\u5206\u9694\uFF1B\u672A\u9009\u62E9\u7684\u5DE5\u5177\u4FDD\u6301\u4E0D\u53D8\uFF09\uFF1A
5168
- 1. Claude Code
5169
- 2. Codex
5170
- 3. Cursor
5171
- 4. CodeBuddy
5682
+ ` + agentMenuLines(installed.profiles) + `
5172
5683
  \u8BF7\u8F93\u5165\u7F16\u53F7 [${defaultSelection}]\uFF0C\u6216\u8F93\u5165 0 \u53D6\u6D88\uFF1A`
5173
5684
  );
5174
5685
  if (answer.trim() === "0" || /^c/i.test(answer.trim())) return 2;
@@ -5420,7 +5931,7 @@ async function runPush(options, dependencies) {
5420
5931
  warnings: result.preview.skipped,
5421
5932
  errors: []
5422
5933
  };
5423
- 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");
5934
+ 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");
5424
5935
  return 0;
5425
5936
  } catch (error) {
5426
5937
  if (attempt === 0 && options.nonInteractive !== true && options.dryRun !== true && error instanceof PushWorkflowError && (error.code === "SERVER_URL_REQUIRED" || error.code === "TOKEN_INVALID")) {
@@ -5480,19 +5991,46 @@ async function runPush(options, dependencies) {
5480
5991
  }
5481
5992
 
5482
5993
  // src/commands/update.ts
5994
+ function parseResolveOverrides(entries) {
5995
+ const map = /* @__PURE__ */ new Map();
5996
+ for (const entry of entries ?? []) {
5997
+ const eq = entry.indexOf("=");
5998
+ if (eq <= 0) {
5999
+ throw new UpdateWorkflowError(
6000
+ "invalid --resolve value; expected path=keep-local|accept-remote",
6001
+ 3,
6002
+ "RESOLVE_OPTION_INVALID"
6003
+ );
6004
+ }
6005
+ const path = entry.slice(0, eq).trim();
6006
+ const strategy = entry.slice(eq + 1).trim();
6007
+ if (path.length === 0 || strategy !== "keep-local" && strategy !== "accept-remote") {
6008
+ throw new UpdateWorkflowError(
6009
+ "invalid --resolve value; expected path=keep-local|accept-remote",
6010
+ 3,
6011
+ "RESOLVE_OPTION_INVALID"
6012
+ );
6013
+ }
6014
+ map.set(path, strategy);
6015
+ }
6016
+ return map;
6017
+ }
5483
6018
  async function runUpdate(options, dependencies) {
5484
6019
  const requestId = uuidV7();
5485
6020
  if (options.nonInteractive === true && options.yes !== true && options.dryRun !== true) {
5486
6021
  dependencies.stderr("\u975E\u4EA4\u4E92\u6A21\u5F0F\u66F4\u65B0\u9700\u8981 --yes\n");
5487
6022
  return 2;
5488
6023
  }
6024
+ const resolveOverrides = parseResolveOverrides(options.resolve);
5489
6025
  const execute = async (dryRun) => updateProject({
5490
6026
  projectRoot: dependencies.cwd,
5491
6027
  ...options.serverUrl === void 0 ? {} : { serverUrl: options.serverUrl },
5492
6028
  ...options.tokenEnv === void 0 ? {} : { tokenEnv: options.tokenEnv },
5493
6029
  env: dependencies.env,
5494
6030
  dryRun,
5495
- fetch: dependencies.fetch
6031
+ fetch: dependencies.fetch,
6032
+ ...options.conflictStrategy === void 0 ? {} : { conflictStrategy: options.conflictStrategy },
6033
+ ...resolveOverrides.size === 0 ? {} : { resolveOverrides }
5496
6034
  });
5497
6035
  try {
5498
6036
  let result;
@@ -5510,22 +6048,24 @@ async function runUpdate(options, dependencies) {
5510
6048
  }
5511
6049
  const applied = new Set(result.applied);
5512
6050
  const skippedByPath = new Map(result.skipped.map((item2) => [item2.path, item2]));
6051
+ const acknowledgedByPath = new Map(result.acknowledged.map((item2) => [item2.path, item2]));
5513
6052
  const items = result.operations.map((operation) => {
5514
6053
  const path = operation.operation === "rename" ? operation.to_path : operation.path;
5515
6054
  const skipped = skippedByPath.get(path);
6055
+ const acknowledged = acknowledgedByPath.get(path);
5516
6056
  return {
5517
6057
  path,
5518
6058
  operation: operation.operation,
5519
6059
  file_kind: operation.file_kind,
5520
6060
  policy: "update",
5521
- status: skipped !== void 0 ? "skipped" : applied.has(path) ? options.dryRun === true ? "planned" : "applied" : "already-applied",
5522
- reason: skipped?.reason ?? null,
6061
+ status: skipped !== void 0 ? "skipped" : acknowledged !== void 0 ? "acknowledged" : applied.has(path) ? options.dryRun === true ? "planned" : "applied" : "already-applied",
6062
+ reason: skipped?.reason ?? acknowledged?.reason ?? null,
5523
6063
  size_bytes: "size_bytes" in operation ? operation.size_bytes : 0
5524
6064
  };
5525
6065
  });
5526
- const exitCode = result.skipped.length > 0 ? 5 : 0;
6066
+ const exitCode = result.conflicts.length > 0 ? 5 : 0;
5527
6067
  const output = {
5528
- schema_version: 1,
6068
+ schema_version: 2,
5529
6069
  command: "update",
5530
6070
  request_id: requestId,
5531
6071
  dry_run: options.dryRun === true,
@@ -5536,13 +6076,15 @@ async function runUpdate(options, dependencies) {
5536
6076
  discovered: result.operations.length,
5537
6077
  applied: options.dryRun === true ? 0 : result.applied.length,
5538
6078
  planned: options.dryRun === true ? result.applied.length : 0,
6079
+ acknowledged: result.acknowledged.length,
6080
+ resolved: result.resolvedKeepLocal.length + result.resolvedAcceptRemote.length,
5539
6081
  skipped: result.skipped.length
5540
6082
  },
5541
6083
  items,
5542
- warnings: result.skipped,
6084
+ warnings: [...result.acknowledged, ...result.skipped],
5543
6085
  errors: []
5544
6086
  };
5545
- 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");
6087
+ 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");
5546
6088
  return exitCode;
5547
6089
  } catch (error) {
5548
6090
  const exitCode = error instanceof UpdateWorkflowError ? error.exitCode : 1;
@@ -5572,7 +6114,7 @@ async function runUpdate(options, dependencies) {
5572
6114
 
5573
6115
  // src/commands/recovery.ts
5574
6116
  import { stat as stat5 } from "node:fs/promises";
5575
- import { join as join17 } from "node:path";
6117
+ import { join as join18 } from "node:path";
5576
6118
  async function exists5(path) {
5577
6119
  try {
5578
6120
  await stat5(path);
@@ -5632,7 +6174,7 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
5632
6174
  return 5;
5633
6175
  }
5634
6176
  }
5635
- const initialized = await exists5(join17(dependencies.cwd, ".harness", "project.yaml"));
6177
+ const initialized = await exists5(join18(dependencies.cwd, ".harness", "project.yaml"));
5636
6178
  if (!initialized || options.nonInteractive === true || explicitConfigure(options)) {
5637
6179
  return null;
5638
6180
  }
@@ -5675,8 +6217,8 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
5675
6217
  }
5676
6218
 
5677
6219
  // src/workflow-data/resolve.ts
5678
- import { cp, mkdir as mkdir5, readdir as readdir8, readFile as readFile16, rm as rm8, stat as stat6 } from "node:fs/promises";
5679
- import { dirname as dirname5, join as join18, relative as relative2 } from "node:path";
6220
+ import { cp, mkdir as mkdir5, readdir as readdir8, readFile as readFile17, rm as rm8, stat as stat6 } from "node:fs/promises";
6221
+ import { dirname as dirname5, join as join19, relative as relative2 } from "node:path";
5680
6222
  import { fileURLToPath } from "node:url";
5681
6223
  var WorkflowDataResolutionError = class extends Error {
5682
6224
  code;
@@ -5713,14 +6255,14 @@ async function listFilesRecursive(root, base = root) {
5713
6255
  const entries = await readdir8(root, { withFileTypes: true });
5714
6256
  const files = [];
5715
6257
  for (const entry of entries) {
5716
- const absolute = join18(root, entry.name);
6258
+ const absolute = join19(root, entry.name);
5717
6259
  if (entry.isDirectory()) {
5718
6260
  files.push(...await listFilesRecursive(absolute, base));
5719
6261
  continue;
5720
6262
  }
5721
6263
  if (!entry.isFile()) continue;
5722
6264
  const rel = relative2(base, absolute).replaceAll("\\", "/");
5723
- files.push({ path: rel, content: await readFile16(absolute, "utf8") });
6265
+ files.push({ path: rel, content: await readFile17(absolute, "utf8") });
5724
6266
  }
5725
6267
  return files;
5726
6268
  }
@@ -5733,7 +6275,7 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
5733
6275
  const manifest = await readWorkflowFamilyManifest(resourcesRoot);
5734
6276
  const expected = manifest.content_sha256;
5735
6277
  if (typeof expected !== "string" || expected.length === 0) return;
5736
- const harnessRoot = join18(resourcesRoot, "harness");
6278
+ const harnessRoot = join19(resourcesRoot, "harness");
5737
6279
  if (!await pathExists3(harnessRoot)) {
5738
6280
  throw new WorkflowDataResolutionError(
5739
6281
  "\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u7F3A\u5C11 harness/\uFF0C\u65E0\u6CD5\u6821\u9A8C SHA-256",
@@ -5755,37 +6297,37 @@ async function monorepoResourcesRoot() {
5755
6297
  const here = dirname5(fileURLToPath(import.meta.url));
5756
6298
  const candidates = [
5757
6299
  // TypeScript source: packages/cli/src/workflow-data -> packages/workflow-data-harness.
5758
- join18(here, "../../../workflow-data-harness"),
6300
+ join19(here, "../../../workflow-data-harness"),
5759
6301
  // Bundled CLI: packages/cli/dist -> packages/workflow-data-harness.
5760
- join18(here, "../../workflow-data-harness"),
6302
+ join19(here, "../../workflow-data-harness"),
5761
6303
  // Test/build layouts that preserve additional source directory levels.
5762
- join18(here, "../../../../packages/workflow-data-harness")
6304
+ join19(here, "../../../../packages/workflow-data-harness")
5763
6305
  ];
5764
6306
  for (const candidate of candidates) {
5765
- if (await pathExists3(join18(candidate, "harness", "manifests"))) return candidate;
6307
+ if (await pathExists3(join19(candidate, "harness", "manifests"))) return candidate;
5766
6308
  }
5767
6309
  return null;
5768
6310
  }
5769
6311
  async function siblingWorkflowPackage(cwd) {
5770
6312
  const here = dirname5(fileURLToPath(import.meta.url));
5771
6313
  const candidates = [
5772
- join18(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
6314
+ join19(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
5773
6315
  // Published layout: node_modules/hunter-harness/dist/bin.js alongside the
5774
6316
  // scoped workflow package in node_modules/@hunter-harness/workflow-harness.
5775
- join18(here, "..", "..", "@hunter-harness", "workflow-harness"),
6317
+ join19(here, "..", "..", "@hunter-harness", "workflow-harness"),
5776
6318
  // Monorepo bundled layout: packages/cli/dist/bin.js.
5777
- join18(here, "..", "..", "workflow-data-harness")
6319
+ join19(here, "..", "..", "workflow-data-harness")
5778
6320
  ];
5779
6321
  for (const candidate of candidates) {
5780
- if (await pathExists3(join18(candidate, "harness", "manifests"))) return candidate;
6322
+ if (await pathExists3(join19(candidate, "harness", "manifests"))) return candidate;
5781
6323
  }
5782
6324
  return null;
5783
6325
  }
5784
6326
  async function readCachedNpmPackageVersion(cacheRoot) {
5785
- const manifestPath = join18(cacheRoot, "package.json");
6327
+ const manifestPath = join19(cacheRoot, "package.json");
5786
6328
  if (!await pathExists3(manifestPath)) return null;
5787
6329
  try {
5788
- const pkg = JSON.parse(await readFile16(manifestPath, "utf8"));
6330
+ const pkg = JSON.parse(await readFile17(manifestPath, "utf8"));
5789
6331
  return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : null;
5790
6332
  } catch {
5791
6333
  return null;
@@ -5808,12 +6350,12 @@ async function latestWorkflowCacheIsStale(cacheRoot, packageName, resolveManifes
5808
6350
  }
5809
6351
  async function materializeWorkflowPackageCache(cacheRoot, packageSpec, extract) {
5810
6352
  await mkdir5(cacheRoot, { recursive: true });
5811
- const staging = join18(cacheRoot, ".extract");
6353
+ const staging = join19(cacheRoot, ".extract");
5812
6354
  await rm8(staging, { recursive: true, force: true });
5813
6355
  await mkdir5(staging, { recursive: true });
5814
6356
  await extract(packageSpec, staging);
5815
- const packageDir = await pathExists3(join18(staging, "harness", "manifests")) ? staging : join18(staging, "package");
5816
- if (!await pathExists3(join18(packageDir, "harness", "manifests"))) {
6357
+ const packageDir = await pathExists3(join19(staging, "harness", "manifests")) ? staging : join19(staging, "package");
6358
+ if (!await pathExists3(join19(packageDir, "harness", "manifests"))) {
5817
6359
  throw new WorkflowDataResolutionError(
5818
6360
  "\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u5185\u5BB9\u65E0\u6548\uFF1A\u7F3A\u5C11 harness/manifests",
5819
6361
  "WORKFLOW_DATA_INVALID",
@@ -5842,8 +6384,8 @@ async function resolveWorkflowResourcesRoot(options, argv = []) {
5842
6384
  const packageName = workflowPackageName(family, options.env);
5843
6385
  const packageSpec = version === "latest" ? packageName : `${packageName}@${version}`;
5844
6386
  const cacheKey = packageSpec.replace("/", "+");
5845
- const cacheRoot = join18(options.cwd, ".harness", "cache", "workflow-packages", cacheKey);
5846
- if (await pathExists3(join18(cacheRoot, "harness", "manifests"))) {
6387
+ const cacheRoot = join19(options.cwd, ".harness", "cache", "workflow-packages", cacheKey);
6388
+ if (await pathExists3(join19(cacheRoot, "harness", "manifests"))) {
5847
6389
  if (version === "latest") {
5848
6390
  const stale = await latestWorkflowCacheIsStale(cacheRoot, packageName, options.pacoteManifest);
5849
6391
  if (stale) {
@@ -5889,9 +6431,9 @@ function describeWorkflowDataFetchFailure(error, packageSpec) {
5889
6431
  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}`;
5890
6432
  }
5891
6433
  async function readWorkflowFamilyManifest(resourcesRoot) {
5892
- const manifestPath = join18(resourcesRoot, "hunter-workflow-family.json");
6434
+ const manifestPath = join19(resourcesRoot, "hunter-workflow-family.json");
5893
6435
  if (!await pathExists3(manifestPath)) return {};
5894
- return JSON.parse(await readFile16(manifestPath, "utf8"));
6436
+ return JSON.parse(await readFile17(manifestPath, "utf8"));
5895
6437
  }
5896
6438
 
5897
6439
  // src/bin.ts
@@ -5982,7 +6524,15 @@ async function runCli(argv, overrides = {}) {
5982
6524
  dependencies
5983
6525
  );
5984
6526
  });
5985
- addCommonOptions(program.command("update")).description("\u5E94\u7528\u5DF2\u6279\u51C6\u7684\u670D\u52A1\u7AEF\u4EA7\u7269").action(async (options) => {
6527
+ addCommonOptions(program.command("update")).description("\u5E94\u7528\u5DF2\u6279\u51C6\u7684\u670D\u52A1\u7AEF\u4EA7\u7269").option(
6528
+ "--conflict-strategy <strategy>",
6529
+ "manual | keep-local | accept-remote\uFF08\u9ED8\u8BA4 manual\uFF1Brename \u51B2\u7A81\u59CB\u7EC8 manual\uFF09"
6530
+ ).option(
6531
+ "--resolve <path=strategy>",
6532
+ "\u5355\u8DEF\u5F84\u8986\u76D6 keep-local|accept-remote\uFF08\u53EF\u91CD\u590D\uFF09",
6533
+ (value, previous) => [...previous, value],
6534
+ []
6535
+ ).action(async (options) => {
5986
6536
  exitCode = await runUpdate(
5987
6537
  { ...program.opts(), ...options },
5988
6538
  dependencies