hunter-harness 0.2.12 → 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 +901 -380
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -1115,7 +1115,7 @@ async function assertNoSymlinks(root, relativePath) {
1115
1115
  async function withRetry(action, options) {
1116
1116
  const attempts = options.attempts ?? 3;
1117
1117
  const sleep = options.sleep ?? (async (milliseconds) => {
1118
- await new Promise((resolve7) => setTimeout(resolve7, milliseconds));
1118
+ await new Promise((resolve8) => setTimeout(resolve8, milliseconds));
1119
1119
  });
1120
1120
  let lastError;
1121
1121
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
@@ -3457,8 +3457,8 @@ function resolvePushAuth(input) {
3457
3457
  }
3458
3458
 
3459
3459
  // ../core/dist/push/push.js
3460
- import { lstat as lstat2, readFile as readFile9, readdir as readdir4, rm as rm4 } from "node:fs/promises";
3461
- import { join as join10, relative, resolve as resolve5 } from "node:path";
3460
+ import { lstat as lstat3, readFile as readFile10, readdir as readdir4, rm as rm5 } from "node:fs/promises";
3461
+ import { join as join11, relative, resolve as resolve6 } from "node:path";
3462
3462
  import { parse as parseYaml5, stringify as stringifyYaml4 } from "yaml";
3463
3463
 
3464
3464
  // ../core/dist/state/baseline.js
@@ -3519,6 +3519,659 @@ async function acquireProtocolLock(projectRoot, operation, options = {}) {
3519
3519
  throw new Error("unable to acquire protocol lock");
3520
3520
  }
3521
3521
 
3522
+ // ../core/dist/sync/synchronize.js
3523
+ import { lstat as lstat2, readFile as readFile9, rm as rm4 } from "node:fs/promises";
3524
+ import { join as join10, resolve as resolve5 } from "node:path";
3525
+
3526
+ // ../core/dist/update/conflicts.js
3527
+ function operationTargetPath(operation) {
3528
+ return operation.operation === "rename" ? operation.to_path : operation.path;
3529
+ }
3530
+ function operationSourcePath(operation) {
3531
+ return operation.operation === "rename" ? operation.from_path : operation.path;
3532
+ }
3533
+ function operationAlreadyApplied(operation, baseline, projectVersion) {
3534
+ const target = baseline.files[operationTargetPath(operation)];
3535
+ if (operation.operation === "delete") {
3536
+ return target?.deleted === true && target.last_applied_version === projectVersion;
3537
+ }
3538
+ return target?.deleted === false && target?.last_applied_version === projectVersion && target.baseline_hash === operation.content_sha256;
3539
+ }
3540
+ function managedBlockDirty(currentContent, managedBlockHash) {
3541
+ if (managedBlockHash === void 0) {
3542
+ return true;
3543
+ }
3544
+ const block = extractManagedBlock(currentContent);
3545
+ return block === null || sha256Bytes(block) !== managedBlockHash;
3546
+ }
3547
+
3548
+ // ../core/dist/sync/artifact-rebase.js
3549
+ function expectedBase(operation) {
3550
+ return operation.operation === "add" ? null : operation.base_content_sha256;
3551
+ }
3552
+ function contentHash(operation) {
3553
+ return operation.operation === "delete" ? null : operation.content_sha256;
3554
+ }
3555
+ function baselineEntryFor(operation, finalContent, projectVersion) {
3556
+ const block = finalContent === null ? null : extractManagedBlock(finalContent);
3557
+ const remoteHash = contentHash(operation);
3558
+ const localHash = finalContent === null ? null : sha256Bytes(finalContent);
3559
+ return {
3560
+ baseline_hash: remoteHash,
3561
+ local_hash_at_apply: localHash,
3562
+ file_kind: operation.file_kind,
3563
+ last_applied_version: projectVersion,
3564
+ deleted: operation.operation === "delete",
3565
+ ...block === null ? {} : { managed_block_hash: sha256Bytes(block) }
3566
+ };
3567
+ }
3568
+ function acknowledgedEntry(operation, context, projectVersion, reason) {
3569
+ void reason;
3570
+ const localContent = operation.operation === "delete" ? context.sourceContent : context.targetContent ?? context.sourceContent;
3571
+ const block = localContent === null ? null : extractManagedBlock(localContent);
3572
+ return {
3573
+ baseline_hash: contentHash(operation),
3574
+ local_hash_at_apply: localContent === null ? null : sha256Bytes(localContent),
3575
+ file_kind: operation.file_kind,
3576
+ last_applied_version: projectVersion,
3577
+ deleted: operation.operation === "delete",
3578
+ ...block === null ? {} : { managed_block_hash: sha256Bytes(block) }
3579
+ };
3580
+ }
3581
+ function deletedSourceEntry(operation, projectVersion) {
3582
+ return {
3583
+ baseline_hash: null,
3584
+ local_hash_at_apply: null,
3585
+ file_kind: operation.file_kind,
3586
+ last_applied_version: projectVersion,
3587
+ deleted: true
3588
+ };
3589
+ }
3590
+ function isRenameTargetCollision(operation, targetContent, incomingHash) {
3591
+ if (operation.operation !== "rename" || targetContent === null) {
3592
+ return false;
3593
+ }
3594
+ return incomingHash === null || sha256Bytes(targetContent) !== incomingHash;
3595
+ }
3596
+ function resolveDecision(path, reason, operation, conflictStrategy, resolveOverrides) {
3597
+ if (reason === "target-collision") {
3598
+ return "conflict";
3599
+ }
3600
+ if (reason === "local-dirty" && operation.operation === "rename") {
3601
+ return "conflict";
3602
+ }
3603
+ const override = resolveOverrides?.get(path);
3604
+ if (override !== void 0) {
3605
+ return override;
3606
+ }
3607
+ if (conflictStrategy === "keep-local") {
3608
+ return "keep-local";
3609
+ }
3610
+ if (conflictStrategy === "accept-remote") {
3611
+ return "accept-remote";
3612
+ }
3613
+ return "conflict";
3614
+ }
3615
+ function computeFinalContent(operation, policy, targetContent, sourceContent, incoming) {
3616
+ const incomingHash = contentHash(operation);
3617
+ let equivalent = incomingHash !== null && targetContent !== null && sha256Bytes(targetContent) === incomingHash;
3618
+ let finalContent = incoming;
3619
+ if (policy.update_policy === "managed-block-only") {
3620
+ if (operation.operation === "delete") {
3621
+ finalContent = sourceContent === null ? null : removeManagedBlock(sourceContent);
3622
+ equivalent = finalContent === sourceContent;
3623
+ } else if (incoming !== null) {
3624
+ const incomingBlock = extractManagedBlock(incoming) ?? incoming.trim();
3625
+ const incomingId = extractSingleManagedBlockById(incoming)?.id;
3626
+ const blockId = operation.operation === "add" || operation.operation === "modify" ? operation.block_id ?? incomingId : void 0;
3627
+ finalContent = blockId !== void 0 ? upsertManagedBlockById(targetContent ?? "", blockId, incomingBlock) : upsertManagedBlock(targetContent ?? "", incomingBlock);
3628
+ equivalent = finalContent === targetContent;
3629
+ }
3630
+ } else if (operation.operation === "delete" && sourceContent === null) {
3631
+ equivalent = true;
3632
+ finalContent = null;
3633
+ } else if (equivalent) {
3634
+ finalContent = targetContent;
3635
+ }
3636
+ return { finalContent, equivalent };
3637
+ }
3638
+ function isDirty(operation, policy, previous, sourceContent, targetContent, incoming) {
3639
+ const incomingHash = contentHash(operation);
3640
+ let equivalent = incomingHash !== null && targetContent !== null && sha256Bytes(targetContent) === incomingHash;
3641
+ let dirty = false;
3642
+ if (operation.operation === "add") {
3643
+ if (targetContent !== null && !equivalent) {
3644
+ if (policy.update_policy === "managed-block-only" && incoming !== null) {
3645
+ const incomingBlock = extractManagedBlock(incoming) ?? incoming.trim();
3646
+ equivalent = extractManagedBlock(targetContent) === incomingBlock;
3647
+ dirty = !equivalent;
3648
+ } else {
3649
+ dirty = true;
3650
+ }
3651
+ }
3652
+ } else if (equivalent) {
3653
+ dirty = false;
3654
+ } else if (sourceContent === null) {
3655
+ dirty = operation.operation !== "delete";
3656
+ } else if (policy.update_policy === "managed-block-only") {
3657
+ dirty = previous?.managed_block_hash === void 0 ? (previous?.local_hash_at_apply ?? previous?.baseline_hash) !== sha256Bytes(sourceContent) : managedBlockDirty(sourceContent, previous.managed_block_hash);
3658
+ } else {
3659
+ dirty = (previous?.local_hash_at_apply ?? previous?.baseline_hash) !== sha256Bytes(sourceContent);
3660
+ }
3661
+ if (operation.operation === "rename" && targetContent !== null && !equivalent) {
3662
+ dirty = true;
3663
+ }
3664
+ return { dirty, equivalent };
3665
+ }
3666
+ function pushBaselineUpdates(plan, operation, target, entry) {
3667
+ plan.baselineUpdates.push({ path: target, entry });
3668
+ if (operation.operation === "rename") {
3669
+ plan.baselineUpdates.push({
3670
+ path: operation.from_path,
3671
+ entry: deletedSourceEntry(operation, entry.last_applied_version ?? null)
3672
+ });
3673
+ }
3674
+ }
3675
+ function recordWrite(plan, bucket, operation, target, content, equivalent, entry) {
3676
+ const write = { operation, path: target, content, equivalent };
3677
+ plan[bucket].push(write);
3678
+ pushBaselineUpdates(plan, operation, target, entry);
3679
+ }
3680
+ function planArtifactRebase(input) {
3681
+ const plan = {
3682
+ applied: [],
3683
+ acknowledged: [],
3684
+ resolvedKeepLocal: [],
3685
+ resolvedAcceptRemote: [],
3686
+ alreadyApplied: [],
3687
+ conflicts: [],
3688
+ baselineUpdates: [],
3689
+ baselineAdvanced: false
3690
+ };
3691
+ for (const context of input.contexts) {
3692
+ const operation = context.operation;
3693
+ const incoming = context.incomingContent;
3694
+ const source = operationSourcePath(operation);
3695
+ const target = operationTargetPath(operation);
3696
+ const policy = classifyFile(target);
3697
+ const incomingHash = contentHash(operation);
3698
+ if (operationAlreadyApplied(operation, input.baseline, input.projectVersion)) {
3699
+ plan.alreadyApplied.push({ path: target, operation });
3700
+ continue;
3701
+ }
3702
+ const staticDecision = decideUpdate(policy, false);
3703
+ if (!staticDecision.apply) {
3704
+ const acknowledgeReason = staticDecision.reason === "protocol-only" ? "protocol-only" : "policy-never";
3705
+ const entry2 = acknowledgedEntry(operation, context, input.projectVersion, acknowledgeReason);
3706
+ plan.acknowledged.push({
3707
+ operation,
3708
+ path: target,
3709
+ content: null,
3710
+ equivalent: true,
3711
+ acknowledgeReason
3712
+ });
3713
+ pushBaselineUpdates(plan, operation, target, entry2);
3714
+ continue;
3715
+ }
3716
+ const previous = input.baseline.files[source];
3717
+ const expected = expectedBase(operation);
3718
+ const actualBaseline = previous?.baseline_hash ?? null;
3719
+ if (expected !== actualBaseline) {
3720
+ const decision = resolveDecision(target, "baseline-diverged", operation, input.conflictStrategy, input.resolveOverrides);
3721
+ if (decision === "conflict") {
3722
+ const conflict2 = {
3723
+ path: target,
3724
+ operation: operation.operation,
3725
+ reason: "baseline-diverged",
3726
+ actualBaselineHash: actualBaseline
3727
+ };
3728
+ if (expected !== null) {
3729
+ conflict2.expectedBaselineHash = expected;
3730
+ }
3731
+ plan.conflicts.push(conflict2);
3732
+ continue;
3733
+ }
3734
+ if (decision === "keep-local") {
3735
+ const localContent = operation.operation === "rename" ? context.sourceContent : context.targetContent ?? context.sourceContent;
3736
+ const entry2 = baselineEntryFor(operation, localContent, input.projectVersion);
3737
+ recordWrite(plan, "resolvedKeepLocal", operation, target, localContent, true, entry2);
3738
+ continue;
3739
+ }
3740
+ if (decision === "accept-remote") {
3741
+ const { finalContent: finalContent2, equivalent: equivalent2 } = computeFinalContent(operation, policy, context.targetContent, context.sourceContent, incoming);
3742
+ const entry2 = baselineEntryFor(operation, finalContent2, input.projectVersion);
3743
+ recordWrite(plan, "resolvedAcceptRemote", operation, target, finalContent2, equivalent2, entry2);
3744
+ continue;
3745
+ }
3746
+ }
3747
+ const targetCollision = isRenameTargetCollision(operation, context.targetContent, incomingHash);
3748
+ if (targetCollision) {
3749
+ plan.conflicts.push({
3750
+ path: target,
3751
+ operation: operation.operation,
3752
+ reason: "target-collision"
3753
+ });
3754
+ continue;
3755
+ }
3756
+ const { dirty, equivalent: preEquivalent } = isDirty(operation, policy, previous, context.sourceContent, context.targetContent, incoming);
3757
+ if (dirty) {
3758
+ const reason = operation.operation === "rename" && context.targetContent !== null && !preEquivalent ? "target-collision" : "local-dirty";
3759
+ const decision = resolveDecision(target, reason, operation, input.conflictStrategy, input.resolveOverrides);
3760
+ if (decision === "conflict") {
3761
+ plan.conflicts.push({
3762
+ path: target,
3763
+ operation: operation.operation,
3764
+ reason
3765
+ });
3766
+ continue;
3767
+ }
3768
+ if (decision === "keep-local") {
3769
+ const localContent = operation.operation === "delete" ? context.sourceContent : operation.operation === "rename" ? context.sourceContent : context.targetContent ?? context.sourceContent;
3770
+ const entry3 = baselineEntryFor(operation, localContent, input.projectVersion);
3771
+ recordWrite(plan, "resolvedKeepLocal", operation, target, localContent, true, entry3);
3772
+ continue;
3773
+ }
3774
+ const { finalContent: finalContent2, equivalent: equivalent2 } = computeFinalContent(operation, policy, context.targetContent, context.sourceContent, incoming);
3775
+ const entry2 = baselineEntryFor(operation, finalContent2, input.projectVersion);
3776
+ recordWrite(plan, "resolvedAcceptRemote", operation, target, finalContent2, equivalent2, entry2);
3777
+ continue;
3778
+ }
3779
+ const { finalContent, equivalent } = computeFinalContent(operation, policy, context.targetContent, context.sourceContent, incoming);
3780
+ const entry = baselineEntryFor(operation, finalContent, input.projectVersion);
3781
+ if (equivalent) {
3782
+ plan.alreadyApplied.push({ path: target, operation });
3783
+ pushBaselineUpdates(plan, operation, target, entry);
3784
+ continue;
3785
+ }
3786
+ recordWrite(plan, "applied", operation, target, finalContent, equivalent, entry);
3787
+ }
3788
+ plan.baselineAdvanced = plan.conflicts.length === 0;
3789
+ return plan;
3790
+ }
3791
+
3792
+ // ../core/dist/sync/synchronize.js
3793
+ var MAX_SYNC_ARTIFACT_ITERATIONS = 50;
3794
+ async function pathExists(path) {
3795
+ try {
3796
+ await lstat2(path);
3797
+ return true;
3798
+ } catch (error) {
3799
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
3800
+ return false;
3801
+ }
3802
+ throw error;
3803
+ }
3804
+ }
3805
+ async function optionalContent(root, path) {
3806
+ const full = join10(root, path);
3807
+ return await pathExists(full) ? readFile9(full, "utf8") : null;
3808
+ }
3809
+ function manifestPayloadHash(manifest) {
3810
+ const payload = { ...manifest };
3811
+ delete payload.manifest_sha256;
3812
+ return sha256Bytes(canonicalJson(payload));
3813
+ }
3814
+ async function loadBlob(root, client, artifactId, operation, requestId, dryRun) {
3815
+ if (operation.operation === "delete") {
3816
+ return null;
3817
+ }
3818
+ const hash = operation.content_sha256;
3819
+ const cacheRoot = join10(root, ".harness", "cache", "server-artifacts", artifactId);
3820
+ const cachePath = join10(cacheRoot, "blobs", hash.replace(":", "_"));
3821
+ if (await pathExists(cachePath) && await sha256File(cachePath) === hash) {
3822
+ return readFile9(cachePath, "utf8");
3823
+ }
3824
+ const bytes = await client.downloadArtifactBlob(artifactId, hash, requestId);
3825
+ if (bytes.byteLength !== operation.size_bytes || sha256Bytes(bytes) !== hash) {
3826
+ await rm4(cachePath, { force: true });
3827
+ throw new Error("ARTIFACT_HASH_MISMATCH");
3828
+ }
3829
+ if (!dryRun) {
3830
+ await atomicWriteFile(cachePath, bytes);
3831
+ }
3832
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
3833
+ }
3834
+ function writeFromPlan(item2, keepLocalNoWrite) {
3835
+ if (keepLocalNoWrite || item2.equivalent) {
3836
+ return null;
3837
+ }
3838
+ const operation = item2.operation;
3839
+ const target = operationTargetPath(operation);
3840
+ if (operation.operation === "delete") {
3841
+ return { operation: "delete", path: target };
3842
+ }
3843
+ if (operation.operation === "rename") {
3844
+ return {
3845
+ operation: "rename",
3846
+ from_path: operation.from_path,
3847
+ to_path: operation.to_path,
3848
+ content: item2.content ?? ""
3849
+ };
3850
+ }
3851
+ return {
3852
+ operation: "modify",
3853
+ path: target,
3854
+ content: item2.content ?? ""
3855
+ };
3856
+ }
3857
+ function collectWrites(plan) {
3858
+ const ops = [];
3859
+ for (const item2 of plan.applied) {
3860
+ const op = writeFromPlan(item2, false);
3861
+ if (op !== null)
3862
+ ops.push(op);
3863
+ }
3864
+ for (const item2 of plan.resolvedAcceptRemote) {
3865
+ const op = writeFromPlan(item2, false);
3866
+ if (op !== null)
3867
+ ops.push(op);
3868
+ }
3869
+ for (const item2 of plan.resolvedKeepLocal) {
3870
+ const op = writeFromPlan(item2, true);
3871
+ if (op !== null)
3872
+ ops.push(op);
3873
+ }
3874
+ return ops;
3875
+ }
3876
+ function pathsFromPlan(plan) {
3877
+ const applied = plan.applied.map((item2) => item2.path);
3878
+ const acknowledged = plan.acknowledged.map((item2) => ({
3879
+ path: item2.path,
3880
+ operation: item2.operation.operation,
3881
+ reason: item2.acknowledgeReason ?? "policy-never"
3882
+ }));
3883
+ return {
3884
+ applied,
3885
+ acknowledged,
3886
+ resolvedKeepLocal: plan.resolvedKeepLocal.map((item2) => item2.path),
3887
+ resolvedAcceptRemote: plan.resolvedAcceptRemote.map((item2) => item2.path),
3888
+ alreadyApplied: plan.alreadyApplied.map((item2) => item2.path)
3889
+ };
3890
+ }
3891
+ async function buildContexts(root, manifest, client, requestId, dryRun) {
3892
+ const contexts = [];
3893
+ for (const operation of manifest.files) {
3894
+ const source = operationSourcePath(operation);
3895
+ const target = operationTargetPath(operation);
3896
+ const incoming = await loadBlob(root, client, manifest.artifact_id, operation, requestId, dryRun);
3897
+ contexts.push({
3898
+ operation,
3899
+ incomingContent: incoming,
3900
+ sourceContent: await optionalContent(root, source),
3901
+ targetContent: target === source ? await optionalContent(root, source) : await optionalContent(root, target)
3902
+ });
3903
+ }
3904
+ return contexts;
3905
+ }
3906
+ function applyBaselineUpdates(baseline, plan) {
3907
+ const next = baselineManifestSchema.parse(structuredClone(baseline));
3908
+ for (const update of plan.baselineUpdates) {
3909
+ next.files[update.path] = update.entry;
3910
+ }
3911
+ return next;
3912
+ }
3913
+ async function planSingleArtifact(root, baseline, manifest, client, requestId, dryRun, conflictStrategy, resolveOverrides) {
3914
+ if (manifest.project_version === null) {
3915
+ throw new Error("artifact manifest missing project_version");
3916
+ }
3917
+ const contexts = await buildContexts(root, manifest, client, requestId, dryRun);
3918
+ return planArtifactRebase({
3919
+ baseline,
3920
+ projectVersion: manifest.project_version,
3921
+ contexts,
3922
+ conflictStrategy,
3923
+ ...resolveOverrides === void 0 ? {} : { resolveOverrides }
3924
+ });
3925
+ }
3926
+ async function saveConflictReport(root, requestId, manifest, plan) {
3927
+ const reportPath = join10(root, ".harness", "reports", "conflicts-" + requestId + ".json");
3928
+ await atomicWriteJson(reportPath, {
3929
+ schema_version: 1,
3930
+ request_id: requestId,
3931
+ artifact_id: manifest.artifact_id,
3932
+ project_version: manifest.project_version,
3933
+ conflicts: plan.conflicts,
3934
+ resolve_hint: "npx hunter-harness update --resolve <path>=keep-local|accept-remote"
3935
+ });
3936
+ }
3937
+ async function synchronizeArtifacts(options, initialBaseline) {
3938
+ const root = resolve5(options.projectRoot);
3939
+ const projectId = options.project.project.project_id;
3940
+ if (projectId === null) {
3941
+ throw new Error("PROJECT_NOT_BOUND");
3942
+ }
3943
+ let baseline = initialBaseline;
3944
+ const seenArtifactIds = /* @__PURE__ */ new Set();
3945
+ const aggregate = {
3946
+ requestId: options.requestId,
3947
+ projectId,
3948
+ artifactsProcessed: 0,
3949
+ applied: [],
3950
+ acknowledged: [],
3951
+ resolvedKeepLocal: [],
3952
+ resolvedAcceptRemote: [],
3953
+ alreadyApplied: [],
3954
+ conflicts: [],
3955
+ skipped: [],
3956
+ operations: [],
3957
+ artifactId: null,
3958
+ observedProjectVersion: null,
3959
+ transactionId: null,
3960
+ dryRun: options.dryRun,
3961
+ baselineAdvanced: false
3962
+ };
3963
+ const conflictStrategy = options.conflictStrategy ?? "manual";
3964
+ for (let iteration = 0; iteration < MAX_SYNC_ARTIFACT_ITERATIONS; iteration++) {
3965
+ const discovery = await options.client.getUpdateManifest(projectId, {
3966
+ base_project_version: baseline.complete_project_version,
3967
+ base_manifest_hash: sha256Bytes(canonicalJson(baseline)),
3968
+ adapter: options.project.adapters.enabled[0] ?? "claude-code",
3969
+ profile: options.project.project.profiles[0] ?? "general"
3970
+ }, options.requestId);
3971
+ aggregate.observedProjectVersion = discovery.observed_project_version;
3972
+ if (!discovery.delta_available || discovery.artifact_id === null) {
3973
+ aggregate.baselineAdvanced = aggregate.conflicts.length === 0;
3974
+ return aggregate;
3975
+ }
3976
+ if (seenArtifactIds.has(discovery.artifact_id)) {
3977
+ throw new Error("DUPLICATE_ARTIFACT_ID:" + discovery.artifact_id);
3978
+ }
3979
+ seenArtifactIds.add(discovery.artifact_id);
3980
+ if (options.stopAfterArtifactId === discovery.artifact_id) {
3981
+ return aggregate;
3982
+ }
3983
+ const manifest = artifactManifestSchema.parse(await options.client.getArtifactManifest(discovery.artifact_id, options.requestId));
3984
+ if (manifest.artifact_id !== discovery.artifact_id || manifest.project_id !== projectId || manifest.project_version === null || manifestPayloadHash(manifest) !== manifest.manifest_sha256) {
3985
+ throw new Error("ARTIFACT_HASH_MISMATCH");
3986
+ }
3987
+ let plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, conflictStrategy, options.resolveOverrides);
3988
+ if (plan.conflicts.length > 0 && options.confirmConflictStrategy !== void 0) {
3989
+ const confirmed = await options.confirmConflictStrategy(plan.conflicts);
3990
+ if (confirmed === false) {
3991
+ aggregate.conflicts = plan.conflicts;
3992
+ aggregate.skipped = plan.conflicts.map((item2) => ({
3993
+ path: item2.path,
3994
+ operation: item2.operation,
3995
+ reason: item2.reason
3996
+ }));
3997
+ aggregate.operations = manifest.files;
3998
+ aggregate.artifactId = manifest.artifact_id;
3999
+ return aggregate;
4000
+ }
4001
+ plan = await planSingleArtifact(root, baseline, manifest, options.client, options.requestId, options.dryRun, confirmed, options.resolveOverrides);
4002
+ }
4003
+ const paths = pathsFromPlan(plan);
4004
+ aggregate.applied.push(...paths.applied);
4005
+ aggregate.acknowledged.push(...paths.acknowledged);
4006
+ aggregate.resolvedKeepLocal.push(...paths.resolvedKeepLocal);
4007
+ aggregate.resolvedAcceptRemote.push(...paths.resolvedAcceptRemote);
4008
+ aggregate.alreadyApplied.push(...paths.alreadyApplied);
4009
+ aggregate.operations = manifest.files;
4010
+ aggregate.artifactId = manifest.artifact_id;
4011
+ aggregate.artifactsProcessed += 1;
4012
+ if (plan.conflicts.length > 0) {
4013
+ aggregate.conflicts = plan.conflicts;
4014
+ aggregate.skipped = plan.conflicts.map((item2) => ({
4015
+ path: item2.path,
4016
+ operation: item2.operation,
4017
+ reason: item2.reason
4018
+ }));
4019
+ if (conflictStrategy !== "manual") {
4020
+ if (!options.dryRun) {
4021
+ await saveConflictReport(root, options.requestId, manifest, plan);
4022
+ }
4023
+ return aggregate;
4024
+ }
4025
+ }
4026
+ const hasApplicableWork = plan.applied.length > 0 || plan.acknowledged.length > 0 || plan.resolvedKeepLocal.length > 0 || plan.resolvedAcceptRemote.length > 0 || plan.baselineUpdates.length > 0;
4027
+ if (plan.conflicts.length > 0 && !hasApplicableWork) {
4028
+ if (!options.dryRun) {
4029
+ await saveConflictReport(root, options.requestId, manifest, plan);
4030
+ }
4031
+ return aggregate;
4032
+ }
4033
+ if (options.dryRun) {
4034
+ baseline = applyBaselineUpdates(baseline, plan);
4035
+ if (plan.baselineAdvanced && plan.conflicts.length === 0) {
4036
+ baseline.complete_project_version = manifest.project_version;
4037
+ baseline.artifact_manifest_hash = manifest.manifest_sha256;
4038
+ baseline.latest_artifact_id = manifest.artifact_id;
4039
+ }
4040
+ if (plan.conflicts.length > 0) {
4041
+ return aggregate;
4042
+ }
4043
+ continue;
4044
+ }
4045
+ await atomicWriteJson(join10(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
4046
+ const lock = await acquireProtocolLock(root, "update", { requestId: options.requestId });
4047
+ try {
4048
+ const nextBaseline = applyBaselineUpdates(baseline, plan);
4049
+ if (plan.baselineAdvanced && plan.conflicts.length === 0) {
4050
+ nextBaseline.complete_project_version = manifest.project_version;
4051
+ nextBaseline.artifact_manifest_hash = manifest.manifest_sha256;
4052
+ nextBaseline.latest_artifact_id = manifest.artifact_id;
4053
+ }
4054
+ const transactionId = "tx_sync_" + Date.now() + "_" + options.requestId;
4055
+ const reportPath = ".harness/reports/update-" + options.requestId + ".json";
4056
+ const report = {
4057
+ schema_version: 2,
4058
+ request_id: options.requestId,
4059
+ artifact_id: manifest.artifact_id,
4060
+ observed_project_version: manifest.project_version,
4061
+ status: plan.conflicts.length === 0 ? "applied" : "partial_due_to_conflicts",
4062
+ applied: paths.applied,
4063
+ acknowledged: paths.acknowledged,
4064
+ resolved_keep_local: paths.resolvedKeepLocal,
4065
+ resolved_accept_remote: paths.resolvedAcceptRemote,
4066
+ already_applied: paths.alreadyApplied,
4067
+ conflicts: plan.conflicts,
4068
+ skipped: aggregate.skipped,
4069
+ transaction_id: transactionId
4070
+ };
4071
+ const fileOps = collectWrites(plan);
4072
+ fileOps.push({
4073
+ operation: "modify",
4074
+ path: ".harness/state/baseline/manifest.json",
4075
+ content: JSON.stringify(nextBaseline, null, 2) + "\n"
4076
+ }, {
4077
+ operation: "add",
4078
+ path: reportPath,
4079
+ content: JSON.stringify(report, null, 2) + "\n"
4080
+ }, {
4081
+ operation: "modify",
4082
+ path: ".harness/state/local/last-update.json",
4083
+ content: JSON.stringify(report, null, 2) + "\n"
4084
+ });
4085
+ await runTransaction(root, fileOps, {
4086
+ id: transactionId,
4087
+ kind: "update",
4088
+ ...options.transactionOptions ?? {}
4089
+ });
4090
+ aggregate.transactionId = transactionId;
4091
+ baseline = nextBaseline;
4092
+ if (plan.conflicts.length > 0) {
4093
+ await saveConflictReport(root, options.requestId, manifest, plan);
4094
+ aggregate.baselineAdvanced = false;
4095
+ return aggregate;
4096
+ }
4097
+ } finally {
4098
+ await lock.release();
4099
+ }
4100
+ }
4101
+ throw new Error("MAX_SYNC_ARTIFACT_ITERATIONS_EXCEEDED");
4102
+ }
4103
+ function baselineEntryFromOperation(manifest, operation, localContent) {
4104
+ const block = localContent === null ? null : extractManagedBlock(localContent);
4105
+ return {
4106
+ baseline_hash: operation.operation === "delete" ? null : operation.content_sha256,
4107
+ local_hash_at_apply: localContent === null ? null : sha256Bytes(localContent),
4108
+ file_kind: operation.file_kind,
4109
+ last_applied_version: manifest.project_version,
4110
+ deleted: operation.operation === "delete",
4111
+ ...block === null ? {} : { managed_block_hash: sha256Bytes(block) }
4112
+ };
4113
+ }
4114
+ async function advanceBaselineFromArtifact(options, baseline) {
4115
+ const root = resolve5(options.projectRoot);
4116
+ if (options.manifest.project_version === null) {
4117
+ throw new Error("artifact manifest missing project_version");
4118
+ }
4119
+ let localChanged = false;
4120
+ for (const operation of options.manifest.files) {
4121
+ const source = operationSourcePath(operation);
4122
+ const target = operationTargetPath(operation);
4123
+ const sourceContent = await optionalContent(root, source);
4124
+ const targetContent = target === source ? sourceContent : await optionalContent(root, target);
4125
+ if (operation.operation === "delete") {
4126
+ if (sourceContent !== null) {
4127
+ localChanged = true;
4128
+ }
4129
+ continue;
4130
+ }
4131
+ const expectedHash = operation.content_sha256;
4132
+ const actualContent = targetContent ?? sourceContent;
4133
+ const actualHash = actualContent === null ? null : sha256Bytes(actualContent);
4134
+ if (actualHash !== expectedHash) {
4135
+ localChanged = true;
4136
+ }
4137
+ }
4138
+ if (localChanged) {
4139
+ return { baseline, localChanged: true, transactionId: null };
4140
+ }
4141
+ const nextBaseline = baselineManifestSchema.parse(structuredClone(baseline));
4142
+ for (const operation of options.manifest.files) {
4143
+ const source = operationSourcePath(operation);
4144
+ const target = operationTargetPath(operation);
4145
+ const sourceContent = await optionalContent(root, source);
4146
+ const targetContent = target === source ? sourceContent : await optionalContent(root, target);
4147
+ const localContent = operation.operation === "delete" ? null : targetContent ?? sourceContent;
4148
+ nextBaseline.files[target] = baselineEntryFromOperation(options.manifest, operation, localContent);
4149
+ if (operation.operation === "rename") {
4150
+ nextBaseline.files[operation.from_path] = {
4151
+ baseline_hash: null,
4152
+ local_hash_at_apply: null,
4153
+ file_kind: operation.file_kind,
4154
+ last_applied_version: options.manifest.project_version,
4155
+ deleted: true
4156
+ };
4157
+ }
4158
+ }
4159
+ nextBaseline.complete_project_version = options.manifest.project_version;
4160
+ nextBaseline.artifact_manifest_hash = options.manifest.manifest_sha256;
4161
+ nextBaseline.latest_artifact_id = options.manifest.artifact_id;
4162
+ const transactionId = "tx_push_baseline_" + Date.now();
4163
+ await runTransaction(root, [{
4164
+ operation: "modify",
4165
+ path: ".harness/state/baseline/manifest.json",
4166
+ content: JSON.stringify(nextBaseline, null, 2) + "\n"
4167
+ }], {
4168
+ id: transactionId,
4169
+ kind: "update",
4170
+ ...options.transactionOptions ?? {}
4171
+ });
4172
+ return { baseline: nextBaseline, localChanged: false, transactionId };
4173
+ }
4174
+
3522
4175
  // ../core/dist/push/push.js
3523
4176
  var PushWorkflowError = class extends Error {
3524
4177
  exitCode;
@@ -3545,7 +4198,7 @@ var SHARED_MANAGED_FILES = [
3545
4198
  ];
3546
4199
  async function exists3(path) {
3547
4200
  try {
3548
- await lstat2(path);
4201
+ await lstat3(path);
3549
4202
  return true;
3550
4203
  } catch (error) {
3551
4204
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -3559,7 +4212,7 @@ async function walkFiles(root, current, output) {
3559
4212
  return;
3560
4213
  }
3561
4214
  for (const item2 of await readdir4(current, { withFileTypes: true })) {
3562
- const path = join10(current, item2.name);
4215
+ const path = join11(current, item2.name);
3563
4216
  if (item2.isSymbolicLink()) {
3564
4217
  throw new PushWorkflowError("symlink is not pushable", 6, "UNSAFE_SYMLINK");
3565
4218
  }
@@ -3581,7 +4234,7 @@ async function walkHarnessEntries(root, directory, output) {
3581
4234
  return;
3582
4235
  for (const item2 of await readdir4(directory, { withFileTypes: true })) {
3583
4236
  if (item2.name.startsWith("harness-")) {
3584
- const path = join10(directory, item2.name);
4237
+ const path = join11(directory, item2.name);
3585
4238
  if (item2.isDirectory()) {
3586
4239
  await walkFiles(root, path, output);
3587
4240
  } else if (item2.isFile()) {
@@ -3591,7 +4244,7 @@ async function walkHarnessEntries(root, directory, output) {
3591
4244
  }
3592
4245
  }
3593
4246
  async function managedFiles(projectRoot, project) {
3594
- const root = resolve5(projectRoot);
4247
+ const root = resolve6(projectRoot);
3595
4248
  const paths = [];
3596
4249
  const adapters = getAdapters(enabledHarnessAgents(project));
3597
4250
  const managedFiles2 = [
@@ -3600,25 +4253,25 @@ async function managedFiles(projectRoot, project) {
3600
4253
  ...adapters.some((adapter) => adapter.name === "codebuddy") ? ["CODEBUDDY.md"] : []
3601
4254
  ];
3602
4255
  for (const path of managedFiles2) {
3603
- if (await exists3(join10(root, path))) {
4256
+ if (await exists3(join11(root, path))) {
3604
4257
  paths.push(path);
3605
4258
  }
3606
4259
  }
3607
4260
  for (const path of SHARED_MANAGED_ROOTS) {
3608
- await walkFiles(root, join10(root, path), paths);
4261
+ await walkFiles(root, join11(root, path), paths);
3609
4262
  }
3610
4263
  for (const adapter of adapters) {
3611
4264
  if (adapter.rulesRoot !== null) {
3612
- await walkFiles(root, join10(root, adapter.rulesRoot), paths);
4265
+ await walkFiles(root, join11(root, adapter.rulesRoot), paths);
3613
4266
  }
3614
- await walkHarnessEntries(root, join10(root, adapter.skillsRoot), paths);
4267
+ await walkHarnessEntries(root, join11(root, adapter.skillsRoot), paths);
3615
4268
  if (adapter.agentsRoot !== null) {
3616
- await walkHarnessEntries(root, join10(root, adapter.agentsRoot), paths);
4269
+ await walkHarnessEntries(root, join11(root, adapter.agentsRoot), paths);
3617
4270
  }
3618
4271
  }
3619
4272
  const result = {};
3620
4273
  for (const path of [...new Set(paths)].sort()) {
3621
- result[path] = await readFile9(join10(root, path), "utf8");
4274
+ result[path] = await readFile10(join11(root, path), "utf8");
3622
4275
  }
3623
4276
  return result;
3624
4277
  }
@@ -3627,14 +4280,14 @@ function proposalBaseline(baseline) {
3627
4280
  }
3628
4281
  async function readProject(root) {
3629
4282
  try {
3630
- return projectConfigSchema.parse(parseYaml5(await readFile9(join10(root, ".harness", "project.yaml"), "utf8")));
4283
+ return projectConfigSchema.parse(parseYaml5(await readFile10(join11(root, ".harness", "project.yaml"), "utf8")));
3631
4284
  } catch {
3632
4285
  throw new PushWorkflowError("project configuration is missing or invalid", 3, "PROJECT_CONFIG_INVALID");
3633
4286
  }
3634
4287
  }
3635
4288
  async function readOptionalJson(path) {
3636
4289
  try {
3637
- return JSON.parse(await readFile9(path, "utf8"));
4290
+ return JSON.parse(await readFile10(path, "utf8"));
3638
4291
  } catch (error) {
3639
4292
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
3640
4293
  return null;
@@ -3646,7 +4299,7 @@ async function clientIdFor(root, explicit) {
3646
4299
  if (explicit !== void 0) {
3647
4300
  return explicit;
3648
4301
  }
3649
- const path = join10(root, ".harness", "state", "local", "client.json");
4302
+ const path = join11(root, ".harness", "state", "local", "client.json");
3650
4303
  const existing = await readOptionalJson(path);
3651
4304
  if (typeof existing?.client_id === "string" && /^cli_[A-Za-z0-9_-]+$/.test(existing.client_id)) {
3652
4305
  return existing.client_id;
@@ -3735,9 +4388,34 @@ function assertPreviewAllowed(preview, skip) {
3735
4388
  }
3736
4389
  }
3737
4390
  var CREDENTIALS_HINT = "\u53EF\u5728\u4EA4\u4E92\u6A21\u5F0F\u4E0B\u5F55\u5165\uFF0C\u6216\u5199\u5165 .harness/credentials.local.yaml\uFF08\u52FF\u63D0\u4EA4 git\uFF09";
3738
- var STALE_BASELINE_MESSAGE = "\u670D\u52A1\u7AEF\u5DF2\u6709\u66F4\u65B0\u7684\u63A8\u9001\uFF0C\u8BF7\u5148 git pull \u540E\u6267\u884C npx hunter-harness update \u518D\u63A8";
3739
- function staleBaselineError(code) {
3740
- return new PushWorkflowError(STALE_BASELINE_MESSAGE, 5, code);
4391
+ var 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;
3741
4419
  }
3742
4420
  function makePreview(baseline, files, confirmedProjectLocal, ignorePaths, deletedAt = (/* @__PURE__ */ new Date()).toISOString()) {
3743
4421
  const filteredFiles = {};
@@ -3777,7 +4455,7 @@ async function bindProject(root, project, baseline, projectId) {
3777
4455
  return { project: nextProject, baseline: nextBaseline };
3778
4456
  }
3779
4457
  async function pushProject(options) {
3780
- const root = resolve5(options.projectRoot);
4458
+ const root = resolve6(options.projectRoot);
3781
4459
  let project = await readProject(root);
3782
4460
  let baseline = await readBaseline(root);
3783
4461
  const profile = parseHarnessProfile(project.project.profiles[0]);
@@ -3830,9 +4508,8 @@ async function pushProject(options) {
3830
4508
  const boundAtStart = project.project.project_id;
3831
4509
  if (boundAtStart !== null) {
3832
4510
  const remote = await client.getProject(boundAtStart, uuidV7());
3833
- if (remote.latest_project_version !== baseline.complete_project_version) {
3834
- throw staleBaselineError("PROJECT_VERSION_CONFLICT");
3835
- }
4511
+ baseline = await autoRebaseIfServerAdvanced(root, project, baseline, client, remote.latest_project_version);
4512
+ preview = makePreview(baseline, await managedFiles(root, project), options.confirmedProjectLocal ?? [], installedPaths);
3836
4513
  }
3837
4514
  const initialSkip = await resolveSensitiveScanSkip(preview, options);
3838
4515
  if (initialSkip.cancelled === true) {
@@ -3844,7 +4521,7 @@ async function pushProject(options) {
3844
4521
  if (options.confirmProposal !== void 0 && !await options.confirmProposal(preview)) {
3845
4522
  return { preview, proposalId: null, projectId: project.project.project_id, cancelled: true };
3846
4523
  }
3847
- const workflowPath = join10(root, ".harness", "state", "local", "push-workflow.json");
4524
+ const workflowPath = join11(root, ".harness", "state", "local", "push-workflow.json");
3848
4525
  const priorWorkflow = await readOptionalJson(workflowPath);
3849
4526
  const provisionalRequestId = priorWorkflow?.local_project_key === project.project.local_project_key ? priorWorkflow.request_id : uuidV7();
3850
4527
  const lock = await acquireProtocolLock(root, "push", { requestId: provisionalRequestId });
@@ -3935,7 +4612,9 @@ async function pushProject(options) {
3935
4612
  });
3936
4613
  }
3937
4614
  }
3938
- const finalized = await client.finalizeProposal(session.session_id, {
4615
+ let finalizeRetried = false;
4616
+ let finalized;
4617
+ const finalizeOnce = async () => client.finalizeProposal(session.session_id, {
3939
4618
  schema_version: 1,
3940
4619
  manifest_sha256: proposalManifestHash,
3941
4620
  base_artifact_id: baseline.latest_artifact_id ?? null,
@@ -3944,7 +4623,60 @@ async function pushProject(options) {
3944
4623
  ...sensitiveScanSkipReason === void 0 ? {} : { sensitive_scan_skip_reason: sensitiveScanSkipReason }
3945
4624
  } : {}
3946
4625
  }, requestId, workflow.finalize_idempotency_key);
3947
- 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"), {
3948
4680
  schema_version: 1,
3949
4681
  request_id: requestId,
3950
4682
  project_id: projectId,
@@ -3952,10 +4684,17 @@ async function pushProject(options) {
3952
4684
  status: finalized.status,
3953
4685
  artifact_id: finalized.artifact_id,
3954
4686
  operation_count: preview.operations.length,
4687
+ warning: pushWarning ?? null,
3955
4688
  recorded_at: (/* @__PURE__ */ new Date()).toISOString()
3956
4689
  });
3957
- await rm4(workflowPath, { force: true });
3958
- 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
+ };
3959
4698
  } catch (error) {
3960
4699
  if (error instanceof PushWorkflowError) {
3961
4700
  throw error;
@@ -3982,8 +4721,8 @@ async function pushProject(options) {
3982
4721
  }
3983
4722
 
3984
4723
  // ../core/dist/state/cleanup.js
3985
- import { readFile as readFile10, readdir as readdir5, rm as rm5 } from "node:fs/promises";
3986
- import { join as join11 } from "node:path";
4724
+ import { readFile as readFile11, readdir as readdir5, rm as rm6 } from "node:fs/promises";
4725
+ import { join as join12 } from "node:path";
3987
4726
  function isSafeEntryName(name) {
3988
4727
  return name.length > 0 && !name.includes("/") && !name.includes("\\") && name !== "." && name !== ".." && !name.includes("\0");
3989
4728
  }
@@ -4007,7 +4746,7 @@ async function cleanupProject(options) {
4007
4746
  continue;
4008
4747
  let journal;
4009
4748
  try {
4010
- 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"));
4011
4750
  } catch {
4012
4751
  continue;
4013
4752
  }
@@ -4025,7 +4764,7 @@ async function cleanupProject(options) {
4025
4764
  continue;
4026
4765
  pruned.push(entry.id);
4027
4766
  if (!options.dryRun) {
4028
- await rm5(join11(layout.transactions, entry.id), { recursive: true, force: true });
4767
+ await rm6(join12(layout.transactions, entry.id), { recursive: true, force: true });
4029
4768
  }
4030
4769
  }
4031
4770
  }
@@ -4034,7 +4773,7 @@ async function cleanupProject(options) {
4034
4773
  continue;
4035
4774
  removedCache.push(name);
4036
4775
  if (!options.dryRun) {
4037
- await rm5(join11(layout.serverArtifacts, name), { recursive: true, force: true });
4776
+ await rm6(join12(layout.serverArtifacts, name), { recursive: true, force: true });
4038
4777
  }
4039
4778
  }
4040
4779
  return {
@@ -4088,10 +4827,10 @@ var AGENT_DESCRIPTORS = {
4088
4827
  var INSTALLABLE_AGENTS = Object.keys(AGENT_DESCRIPTORS).filter((agent) => AGENT_DESCRIPTORS[agent]?.installable === true);
4089
4828
 
4090
4829
  // ../core/dist/transaction/recovery.js
4091
- import { readFile as readFile11, readdir as readdir6, rm as rm6, stat as stat3 } from "node:fs/promises";
4092
- import { join as join12 } from "node:path";
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";
4093
4832
  async function recoverTransaction(projectRoot, transactionId) {
4094
- 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"));
4095
4834
  if (journal.state === "committed") {
4096
4835
  return { transactionId, status: "committed" };
4097
4836
  }
@@ -4118,7 +4857,7 @@ async function listTransactions(projectRoot) {
4118
4857
  const transactions = [];
4119
4858
  for (const transactionId of names) {
4120
4859
  try {
4121
- 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"));
4122
4861
  transactions.push({
4123
4862
  transactionId,
4124
4863
  kind: journal.kind,
@@ -4133,7 +4872,7 @@ async function listTransactions(projectRoot) {
4133
4872
  async function pendingTransactions(projectRoot) {
4134
4873
  return (await listTransactions(projectRoot)).filter((item2) => RECOVERY_STATES.has(item2.state));
4135
4874
  }
4136
- async function pathExists(path) {
4875
+ async function pathExists2(path) {
4137
4876
  try {
4138
4877
  await stat3(path);
4139
4878
  return true;
@@ -4149,12 +4888,12 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
4149
4888
  if (latest === void 0) {
4150
4889
  throw new Error("no committed update transaction is available for rollback");
4151
4890
  }
4152
- const transactionRoot = join12(stateLayout(projectRoot).transactions, latest.transactionId);
4153
- const journal = JSON.parse(await readFile11(join12(transactionRoot, "journal.json"), "utf8"));
4154
- const after = JSON.parse(await readFile11(join12(transactionRoot, "after", "manifest.json"), "utf8"));
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"));
4155
4894
  for (const entry of after) {
4156
- const target = join12(projectRoot, entry.path);
4157
- const exists6 = await pathExists(target);
4895
+ const target = join13(projectRoot, entry.path);
4896
+ const exists6 = await pathExists2(target);
4158
4897
  if (exists6 !== entry.exists || exists6 && await sha256File(target) !== entry.hash) {
4159
4898
  throw new Error("cannot rollback dirty path: " + entry.path);
4160
4899
  }
@@ -4166,10 +4905,10 @@ async function rollbackLatestCommittedUpdate(projectRoot) {
4166
4905
  continue;
4167
4906
  }
4168
4907
  seen.add(snapshot.path);
4169
- const target = join12(projectRoot, snapshot.path);
4170
- const exists6 = await pathExists(target);
4908
+ const target = join13(projectRoot, snapshot.path);
4909
+ const exists6 = await pathExists2(target);
4171
4910
  if (snapshot.existed && snapshot.snapshot_name !== null) {
4172
- const content = await readFile11(join12(transactionRoot, "before", snapshot.snapshot_name));
4911
+ const content = await readFile12(join13(transactionRoot, "before", snapshot.snapshot_name));
4173
4912
  operations.push({
4174
4913
  operation: exists6 ? "modify" : "add",
4175
4914
  path: snapshot.path,
@@ -4198,7 +4937,7 @@ async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Dat
4198
4937
  if (!removable) {
4199
4938
  continue;
4200
4939
  }
4201
- await rm6(join12(stateLayout(projectRoot).transactions, item2.transactionId), {
4940
+ await rm7(join13(stateLayout(projectRoot).transactions, item2.transactionId), {
4202
4941
  recursive: true,
4203
4942
  force: true
4204
4943
  });
@@ -4207,31 +4946,9 @@ async function cleanupOldTransactions(projectRoot, now = /* @__PURE__ */ new Dat
4207
4946
  return removed;
4208
4947
  }
4209
4948
 
4210
- // ../core/dist/update/conflicts.js
4211
- function operationTargetPath(operation) {
4212
- return operation.operation === "rename" ? operation.to_path : operation.path;
4213
- }
4214
- function operationSourcePath(operation) {
4215
- return operation.operation === "rename" ? operation.from_path : operation.path;
4216
- }
4217
- function operationAlreadyApplied(operation, baseline, projectVersion) {
4218
- const target = baseline.files[operationTargetPath(operation)];
4219
- if (operation.operation === "delete") {
4220
- return target?.deleted === true && target.last_applied_version === projectVersion;
4221
- }
4222
- return target?.deleted === false && target?.last_applied_version === projectVersion && target.baseline_hash === operation.content_sha256;
4223
- }
4224
- function managedBlockDirty(currentContent, managedBlockHash) {
4225
- if (managedBlockHash === void 0) {
4226
- return true;
4227
- }
4228
- const block = extractManagedBlock(currentContent);
4229
- return block === null || sha256Bytes(block) !== managedBlockHash;
4230
- }
4231
-
4232
4949
  // ../core/dist/update/update.js
4233
- import { lstat as lstat3, readFile as readFile12, rm as rm7 } from "node:fs/promises";
4234
- import { join as join13, resolve as resolve6 } from "node:path";
4950
+ import { readFile as readFile13 } from "node:fs/promises";
4951
+ import { join as join14, resolve as resolve7 } from "node:path";
4235
4952
  import { parse as parseYaml8 } from "yaml";
4236
4953
  var UpdateWorkflowError = class extends Error {
4237
4954
  exitCode;
@@ -4243,90 +4960,11 @@ var UpdateWorkflowError = class extends Error {
4243
4960
  this.code = code;
4244
4961
  }
4245
4962
  };
4246
- async function pathExists2(path) {
4247
- try {
4248
- await lstat3(path);
4249
- return true;
4250
- } catch (error) {
4251
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
4252
- return false;
4253
- }
4254
- throw error;
4255
- }
4256
- }
4257
- async function optionalContent(path) {
4258
- return await pathExists2(path) ? readFile12(path, "utf8") : null;
4259
- }
4260
- function manifestPayloadHash(manifest) {
4261
- const payload = { ...manifest };
4262
- delete payload.manifest_sha256;
4263
- return sha256Bytes(canonicalJson(payload));
4264
- }
4265
- function expectedBase(operation) {
4266
- return operation.operation === "add" ? null : operation.base_content_sha256;
4267
- }
4268
- function contentHash(operation) {
4269
- return operation.operation === "delete" ? null : operation.content_sha256;
4270
- }
4271
- async function loadBlob(root, client, artifactId, operation, requestId, dryRun) {
4272
- if (operation.operation === "delete") {
4273
- return null;
4274
- }
4275
- const hash = operation.content_sha256;
4276
- const cacheRoot = join13(root, ".harness", "cache", "server-artifacts", artifactId);
4277
- const cachePath = join13(cacheRoot, "blobs", hash.replace(":", "_"));
4278
- if (await pathExists2(cachePath) && await sha256File(cachePath) === hash) {
4279
- return readFile12(cachePath, "utf8");
4280
- }
4281
- const bytes = await client.downloadArtifactBlob(artifactId, hash, requestId);
4282
- if (bytes.byteLength !== operation.size_bytes || sha256Bytes(bytes) !== hash) {
4283
- await rm7(cachePath, { force: true });
4284
- throw new UpdateWorkflowError("artifact blob size or hash mismatch", 4, "ARTIFACT_HASH_MISMATCH");
4285
- }
4286
- if (!dryRun) {
4287
- await atomicWriteFile(cachePath, bytes);
4288
- }
4289
- return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
4290
- }
4291
- function nextBaselineEntry(operation, finalContent, projectVersion) {
4292
- const block = finalContent === null ? null : extractManagedBlock(finalContent);
4293
- return {
4294
- baseline_hash: contentHash(operation),
4295
- local_hash_at_apply: finalContent === null ? null : sha256Bytes(finalContent),
4296
- file_kind: operation.file_kind,
4297
- last_applied_version: projectVersion,
4298
- deleted: operation.operation === "delete",
4299
- ...block === null ? {} : { managed_block_hash: sha256Bytes(block) }
4300
- };
4301
- }
4302
- function transactionOperation(item2) {
4303
- if (item2.equivalent) {
4304
- return null;
4305
- }
4306
- const operation = item2.operation;
4307
- const target = operationTargetPath(operation);
4308
- if (operation.operation === "delete") {
4309
- return item2.finalContent === null ? { operation: "delete", path: target } : { operation: "modify", path: target, content: item2.finalContent };
4310
- }
4311
- if (operation.operation === "rename") {
4312
- return {
4313
- operation: "rename",
4314
- from_path: operation.from_path,
4315
- to_path: operation.to_path,
4316
- content: item2.finalContent ?? item2.content ?? ""
4317
- };
4318
- }
4319
- return {
4320
- operation: "modify",
4321
- path: target,
4322
- content: item2.finalContent ?? item2.content ?? ""
4323
- };
4324
- }
4325
4963
  async function updateProject(options) {
4326
- const root = resolve6(options.projectRoot);
4964
+ const root = resolve7(options.projectRoot);
4327
4965
  let project;
4328
4966
  try {
4329
- 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")));
4330
4968
  } catch {
4331
4969
  throw new UpdateWorkflowError("project configuration is invalid", 3, "PROJECT_CONFIG_INVALID");
4332
4970
  }
@@ -4365,183 +5003,18 @@ async function updateProject(options) {
4365
5003
  ...options.fetch === void 0 ? {} : { fetch: options.fetch }
4366
5004
  });
4367
5005
  try {
4368
- const discovery = await client.getUpdateManifest(project.project.project_id, {
4369
- base_project_version: baseline.complete_project_version,
4370
- base_manifest_hash: sha256Bytes(canonicalJson(baseline)),
4371
- adapter: project.adapters.enabled[0] ?? "claude-code",
4372
- profile: project.project.profiles[0] ?? "general"
4373
- }, requestId);
4374
- if (!discovery.delta_available || discovery.artifact_id === null) {
4375
- return {
4376
- requestId,
4377
- projectId: project.project.project_id,
4378
- artifactId: null,
4379
- observedProjectVersion: discovery.observed_project_version,
4380
- operations: [],
4381
- applied: [],
4382
- skipped: [],
4383
- transactionId: null,
4384
- dryRun: options.dryRun
4385
- };
4386
- }
4387
- const manifest = artifactManifestSchema.parse(await client.getArtifactManifest(discovery.artifact_id, requestId));
4388
- if (manifest.artifact_id !== discovery.artifact_id || manifest.project_id !== project.project.project_id || manifest.project_version === null || manifestPayloadHash(manifest) !== manifest.manifest_sha256) {
4389
- throw new UpdateWorkflowError("artifact manifest integrity check failed", 4, "ARTIFACT_HASH_MISMATCH");
4390
- }
4391
- const blobs = /* @__PURE__ */ new Map();
4392
- for (const operation of manifest.files) {
4393
- const policy = classifyFile(operationTargetPath(operation));
4394
- if (decideUpdate(policy, false).apply) {
4395
- blobs.set(operation, await loadBlob(root, client, manifest.artifact_id, operation, requestId, options.dryRun));
4396
- }
4397
- }
4398
- const prepared = [];
4399
- const skipped = [];
4400
- for (const operation of manifest.files) {
4401
- if (operationAlreadyApplied(operation, baseline, manifest.project_version)) {
4402
- continue;
4403
- }
4404
- const source = operationSourcePath(operation);
4405
- const target = operationTargetPath(operation);
4406
- const policy = classifyFile(target);
4407
- const staticDecision = decideUpdate(policy, false);
4408
- if (!staticDecision.apply) {
4409
- skipped.push({ path: target, operation: operation.operation, reason: staticDecision.reason });
4410
- continue;
4411
- }
4412
- const previous = baseline.files[source];
4413
- if (expectedBase(operation) !== (previous?.baseline_hash ?? null)) {
4414
- skipped.push({ path: target, operation: operation.operation, reason: "baseline-diverged" });
4415
- continue;
4416
- }
4417
- const sourceContent = await optionalContent(join13(root, source));
4418
- const targetContent = target === source ? sourceContent : await optionalContent(join13(root, target));
4419
- const incoming = blobs.get(operation) ?? null;
4420
- const incomingHash = contentHash(operation);
4421
- let equivalent = incomingHash !== null && targetContent !== null && sha256Bytes(targetContent) === incomingHash;
4422
- let dirty = false;
4423
- if (operation.operation === "add") {
4424
- if (targetContent !== null && !equivalent) {
4425
- if (policy.update_policy === "managed-block-only" && incoming !== null) {
4426
- const incomingBlock = extractManagedBlock(incoming) ?? incoming.trim();
4427
- equivalent = extractManagedBlock(targetContent) === incomingBlock;
4428
- dirty = !equivalent;
4429
- } else {
4430
- dirty = true;
4431
- }
4432
- }
4433
- } else if (sourceContent === null) {
4434
- dirty = operation.operation !== "delete";
4435
- } else if (policy.update_policy === "managed-block-only") {
4436
- dirty = previous?.managed_block_hash === void 0 ? (previous?.local_hash_at_apply ?? previous?.baseline_hash) !== sha256Bytes(sourceContent) : managedBlockDirty(sourceContent, previous.managed_block_hash);
4437
- } else {
4438
- dirty = (previous?.local_hash_at_apply ?? previous?.baseline_hash) !== sha256Bytes(sourceContent);
4439
- }
4440
- if (operation.operation === "rename" && targetContent !== null && !equivalent) {
4441
- dirty = true;
4442
- }
4443
- if (dirty) {
4444
- skipped.push({ path: target, operation: operation.operation, reason: "local-dirty" });
4445
- continue;
4446
- }
4447
- let finalContent = incoming;
4448
- if (policy.update_policy === "managed-block-only") {
4449
- if (operation.operation === "delete") {
4450
- finalContent = sourceContent === null ? null : removeManagedBlock(sourceContent);
4451
- equivalent = finalContent === sourceContent;
4452
- } else if (incoming !== null) {
4453
- const incomingBlock = extractManagedBlock(incoming) ?? incoming.trim();
4454
- const incomingId = extractSingleManagedBlockById(incoming)?.id;
4455
- const blockId = operation.operation === "add" || operation.operation === "modify" ? operation.block_id ?? incomingId : void 0;
4456
- finalContent = blockId !== void 0 ? upsertManagedBlockById(targetContent ?? "", blockId, incomingBlock) : upsertManagedBlock(targetContent ?? "", incomingBlock);
4457
- equivalent = finalContent === targetContent;
4458
- }
4459
- } else if (operation.operation === "delete" && sourceContent === null) {
4460
- equivalent = true;
4461
- }
4462
- prepared.push({ operation, content: incoming, finalContent, equivalent });
4463
- }
4464
- const applied = prepared.map((item2) => operationTargetPath(item2.operation));
4465
- if (options.dryRun) {
4466
- return {
4467
- requestId,
4468
- projectId: project.project.project_id,
4469
- artifactId: manifest.artifact_id,
4470
- observedProjectVersion: manifest.project_version,
4471
- operations: manifest.files,
4472
- applied,
4473
- skipped,
4474
- transactionId: null,
4475
- dryRun: true
4476
- };
4477
- }
4478
- await atomicWriteJson(join13(root, ".harness", "cache", "server-artifacts", manifest.artifact_id, "manifest.json"), manifest);
4479
- const lock = await acquireProtocolLock(root, "update", { requestId });
4480
- try {
4481
- const nextBaseline = baselineManifestSchema.parse(structuredClone(baseline));
4482
- for (const item2 of prepared) {
4483
- const target = operationTargetPath(item2.operation);
4484
- nextBaseline.files[target] = nextBaselineEntry(item2.operation, item2.finalContent, manifest.project_version);
4485
- if (item2.operation.operation === "rename") {
4486
- nextBaseline.files[item2.operation.from_path] = {
4487
- baseline_hash: null,
4488
- local_hash_at_apply: null,
4489
- file_kind: item2.operation.file_kind,
4490
- last_applied_version: manifest.project_version,
4491
- deleted: true
4492
- };
4493
- }
4494
- }
4495
- if (skipped.length === 0) {
4496
- nextBaseline.complete_project_version = manifest.project_version;
4497
- nextBaseline.artifact_manifest_hash = manifest.manifest_sha256;
4498
- nextBaseline.latest_artifact_id = manifest.artifact_id;
4499
- }
4500
- const transactionId = "tx_update_" + Date.now() + "_" + uuidV7();
4501
- const reportPath = ".harness/reports/update-" + requestId + ".json";
4502
- const report = {
4503
- schema_version: 1,
4504
- request_id: requestId,
4505
- artifact_id: manifest.artifact_id,
4506
- observed_project_version: manifest.project_version,
4507
- status: skipped.length === 0 ? "applied" : "partial_due_to_dirty",
4508
- applied,
4509
- skipped,
4510
- transaction_id: transactionId
4511
- };
4512
- const operations = prepared.map((item2) => transactionOperation(item2)).filter((item2) => item2 !== null);
4513
- operations.push({
4514
- operation: "modify",
4515
- path: ".harness/state/baseline/manifest.json",
4516
- content: JSON.stringify(nextBaseline, null, 2) + "\n"
4517
- }, {
4518
- operation: "add",
4519
- path: reportPath,
4520
- content: JSON.stringify(report, null, 2) + "\n"
4521
- }, {
4522
- operation: "modify",
4523
- path: ".harness/state/local/last-update.json",
4524
- content: JSON.stringify(report, null, 2) + "\n"
4525
- });
4526
- await runTransaction(root, operations, {
4527
- id: transactionId,
4528
- kind: "update",
4529
- ...options.transactionOptions ?? {}
4530
- });
4531
- return {
4532
- requestId,
4533
- projectId: project.project.project_id,
4534
- artifactId: manifest.artifact_id,
4535
- observedProjectVersion: manifest.project_version,
4536
- operations: manifest.files,
4537
- applied,
4538
- skipped,
4539
- transactionId,
4540
- dryRun: false
4541
- };
4542
- } finally {
4543
- await lock.release();
4544
- }
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;
4545
5018
  } catch (error) {
4546
5019
  if (error instanceof UpdateWorkflowError) {
4547
5020
  throw error;
@@ -4549,16 +5022,25 @@ async function updateProject(options) {
4549
5022
  if (error instanceof ApiError) {
4550
5023
  throw new UpdateWorkflowError(error.message, error.status === 401 || error.status === 403 ? 8 : error.status === 409 ? 5 : 4, error.code);
4551
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
+ }
4552
5028
  if (error instanceof Error && error.name === "ZodError") {
4553
5029
  throw new UpdateWorkflowError("artifact schema validation failed", 7, "SCHEMA_VALIDATION_FAILED");
4554
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
+ }
4555
5037
  throw new UpdateWorkflowError(error instanceof Error ? error.message : "update failed", 4, "NETWORK_OR_SERVER_ERROR");
4556
5038
  }
4557
5039
  }
4558
5040
 
4559
5041
  // src/config/init-config.ts
4560
- import { isAbsolute as isAbsolute2, join as join14 } from "node:path";
4561
- import { readFile as readFile13 } from "node:fs/promises";
5042
+ import { isAbsolute as isAbsolute2, join as join15 } from "node:path";
5043
+ import { readFile as readFile14 } from "node:fs/promises";
4562
5044
  var InitConfigurationError = class extends Error {
4563
5045
  exitCode;
4564
5046
  code;
@@ -4641,9 +5123,9 @@ function hasOwn(record, key) {
4641
5123
  async function resolveInitConfig(cwd, flags, prompts = {}, warnings = []) {
4642
5124
  let fileConfig = {};
4643
5125
  if (flags.config !== void 0) {
4644
- const path = isAbsolute2(flags.config) ? flags.config : join14(cwd, flags.config);
5126
+ const path = isAbsolute2(flags.config) ? flags.config : join15(cwd, flags.config);
4645
5127
  try {
4646
- fileConfig = JSON.parse(await readFile13(path, "utf8"));
5128
+ fileConfig = JSON.parse(await readFile14(path, "utf8"));
4647
5129
  } catch (error) {
4648
5130
  throw new InitConfigurationError(
4649
5131
  "unable to read init config: " + (error instanceof Error ? error.message : String(error)),
@@ -4725,8 +5207,8 @@ function serializeCliResult(result) {
4725
5207
  }
4726
5208
 
4727
5209
  // src/config/codebuddy-setup.ts
4728
- import { mkdir as mkdir4, readFile as readFile14, readdir as readdir7, stat as stat4, writeFile as writeFile5 } from "node:fs/promises";
4729
- import { basename as basename2, dirname as dirname4, extname, join as join15 } from "node:path";
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";
4730
5212
  var MANAGED_RULE_NAMES = /* @__PURE__ */ new Set([
4731
5213
  "harness-general.md",
4732
5214
  "harness-general.mdc",
@@ -4745,7 +5227,7 @@ async function exists4(path) {
4745
5227
  }
4746
5228
  async function readJsonObject(path) {
4747
5229
  try {
4748
- const parsed = JSON.parse(await readFile14(path, "utf8"));
5230
+ const parsed = JSON.parse(await readFile15(path, "utf8"));
4749
5231
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
4750
5232
  } catch (error) {
4751
5233
  if (error instanceof Error && "code" in error && error.code === "ENOENT") return {};
@@ -4753,27 +5235,27 @@ async function readJsonObject(path) {
4753
5235
  }
4754
5236
  }
4755
5237
  async function inspectCodeBuddySetup(projectRoot) {
4756
- const rulesRoot = join15(projectRoot, ".claude", "rules");
5238
+ const rulesRoot = join16(projectRoot, ".claude", "rules");
4757
5239
  let claudeRules = [];
4758
5240
  try {
4759
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();
4760
5242
  } catch (error) {
4761
5243
  if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
4762
5244
  }
4763
- const mcp = await readJsonObject(join15(projectRoot, ".mcp.json"));
5245
+ const mcp = await readJsonObject(join16(projectRoot, ".mcp.json"));
4764
5246
  const servers = mcp?.mcpServers;
4765
5247
  const configured = servers !== null && typeof servers === "object" && !Array.isArray(servers) && Object.prototype.hasOwnProperty.call(servers, "codegraph");
4766
5248
  return {
4767
5249
  claudeRules,
4768
- hasCodeGraphIndex: await exists4(join15(projectRoot, ".codegraph")),
5250
+ hasCodeGraphIndex: await exists4(join16(projectRoot, ".codegraph")),
4769
5251
  codeGraphConfigured: configured
4770
5252
  };
4771
5253
  }
4772
5254
  function destinationTargets(root, surface, name) {
4773
5255
  const stem = basename2(name, extname(name));
4774
5256
  const targets = [];
4775
- if (surface !== "cli") targets.push(join15(root, ".codebuddy", ".rules", `${stem}.mdc`));
4776
- if (surface !== "ide") targets.push(join15(root, ".codebuddy", "rules", `${stem}.md`));
5257
+ if (surface !== "cli") targets.push(join16(root, ".codebuddy", ".rules", `${stem}.mdc`));
5258
+ if (surface !== "ide") targets.push(join16(root, ".codebuddy", "rules", `${stem}.md`));
4777
5259
  return targets;
4778
5260
  }
4779
5261
  async function applyCodeBuddySetup(options) {
@@ -4787,8 +5269,8 @@ async function applyCodeBuddySetup(options) {
4787
5269
  if (options.syncClaudeRules) {
4788
5270
  const plan = await inspectCodeBuddySetup(options.projectRoot);
4789
5271
  for (const name of plan.claudeRules) {
4790
- const source = join15(options.projectRoot, ".claude", "rules", name);
4791
- const content = await readFile14(source, "utf8");
5272
+ const source = join16(options.projectRoot, ".claude", "rules", name);
5273
+ const content = await readFile15(source, "utf8");
4792
5274
  if (SENSITIVE_ASSIGNMENT.test(content) || PRIVATE_KEY_BLOCK.test(content)) {
4793
5275
  result.skippedSensitive.push(name);
4794
5276
  continue;
@@ -4805,7 +5287,7 @@ async function applyCodeBuddySetup(options) {
4805
5287
  }
4806
5288
  }
4807
5289
  if (options.configureCodeGraph) {
4808
- const path = join15(options.projectRoot, ".mcp.json");
5290
+ const path = join16(options.projectRoot, ".mcp.json");
4809
5291
  const current = await readJsonObject(path);
4810
5292
  if (current === null) {
4811
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");
@@ -4829,13 +5311,13 @@ async function applyCodeBuddySetup(options) {
4829
5311
  }
4830
5312
 
4831
5313
  // src/commands/refresh.ts
4832
- import { readFile as readFile15 } from "node:fs/promises";
4833
- import { join as join16 } from "node:path";
5314
+ import { readFile as readFile16 } from "node:fs/promises";
5315
+ import { join as join17 } from "node:path";
4834
5316
  import { parse as parseYaml9 } from "yaml";
4835
5317
  async function detectProject(root) {
4836
5318
  let content;
4837
5319
  try {
4838
- content = await readFile15(join16(root, ".harness", "project.yaml"), "utf8");
5320
+ content = await readFile16(join17(root, ".harness", "project.yaml"), "utf8");
4839
5321
  } catch (error) {
4840
5322
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
4841
5323
  return { status: "absent" };
@@ -5449,7 +5931,7 @@ async function runPush(options, dependencies) {
5449
5931
  warnings: result.preview.skipped,
5450
5932
  errors: []
5451
5933
  };
5452
- dependencies.stdout(options.json === true ? serializeCliResult(output) : options.dryRun === true ? "Push preview contains " + items.length + " operations.\n" : "Pushed artifact " + result.artifactId + " (proposal " + result.proposalId + ").\n");
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");
5453
5935
  return 0;
5454
5936
  } catch (error) {
5455
5937
  if (attempt === 0 && options.nonInteractive !== true && options.dryRun !== true && error instanceof PushWorkflowError && (error.code === "SERVER_URL_REQUIRED" || error.code === "TOKEN_INVALID")) {
@@ -5509,19 +5991,46 @@ async function runPush(options, dependencies) {
5509
5991
  }
5510
5992
 
5511
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
+ }
5512
6018
  async function runUpdate(options, dependencies) {
5513
6019
  const requestId = uuidV7();
5514
6020
  if (options.nonInteractive === true && options.yes !== true && options.dryRun !== true) {
5515
6021
  dependencies.stderr("\u975E\u4EA4\u4E92\u6A21\u5F0F\u66F4\u65B0\u9700\u8981 --yes\n");
5516
6022
  return 2;
5517
6023
  }
6024
+ const resolveOverrides = parseResolveOverrides(options.resolve);
5518
6025
  const execute = async (dryRun) => updateProject({
5519
6026
  projectRoot: dependencies.cwd,
5520
6027
  ...options.serverUrl === void 0 ? {} : { serverUrl: options.serverUrl },
5521
6028
  ...options.tokenEnv === void 0 ? {} : { tokenEnv: options.tokenEnv },
5522
6029
  env: dependencies.env,
5523
6030
  dryRun,
5524
- fetch: dependencies.fetch
6031
+ fetch: dependencies.fetch,
6032
+ ...options.conflictStrategy === void 0 ? {} : { conflictStrategy: options.conflictStrategy },
6033
+ ...resolveOverrides.size === 0 ? {} : { resolveOverrides }
5525
6034
  });
5526
6035
  try {
5527
6036
  let result;
@@ -5539,22 +6048,24 @@ async function runUpdate(options, dependencies) {
5539
6048
  }
5540
6049
  const applied = new Set(result.applied);
5541
6050
  const skippedByPath = new Map(result.skipped.map((item2) => [item2.path, item2]));
6051
+ const acknowledgedByPath = new Map(result.acknowledged.map((item2) => [item2.path, item2]));
5542
6052
  const items = result.operations.map((operation) => {
5543
6053
  const path = operation.operation === "rename" ? operation.to_path : operation.path;
5544
6054
  const skipped = skippedByPath.get(path);
6055
+ const acknowledged = acknowledgedByPath.get(path);
5545
6056
  return {
5546
6057
  path,
5547
6058
  operation: operation.operation,
5548
6059
  file_kind: operation.file_kind,
5549
6060
  policy: "update",
5550
- status: skipped !== void 0 ? "skipped" : applied.has(path) ? options.dryRun === true ? "planned" : "applied" : "already-applied",
5551
- 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,
5552
6063
  size_bytes: "size_bytes" in operation ? operation.size_bytes : 0
5553
6064
  };
5554
6065
  });
5555
- const exitCode = result.skipped.length > 0 ? 5 : 0;
6066
+ const exitCode = result.conflicts.length > 0 ? 5 : 0;
5556
6067
  const output = {
5557
- schema_version: 1,
6068
+ schema_version: 2,
5558
6069
  command: "update",
5559
6070
  request_id: requestId,
5560
6071
  dry_run: options.dryRun === true,
@@ -5565,13 +6076,15 @@ async function runUpdate(options, dependencies) {
5565
6076
  discovered: result.operations.length,
5566
6077
  applied: options.dryRun === true ? 0 : result.applied.length,
5567
6078
  planned: options.dryRun === true ? result.applied.length : 0,
6079
+ acknowledged: result.acknowledged.length,
6080
+ resolved: result.resolvedKeepLocal.length + result.resolvedAcceptRemote.length,
5568
6081
  skipped: result.skipped.length
5569
6082
  },
5570
6083
  items,
5571
- warnings: result.skipped,
6084
+ warnings: [...result.acknowledged, ...result.skipped],
5572
6085
  errors: []
5573
6086
  };
5574
- dependencies.stdout(options.json === true ? serializeCliResult(output) : result.artifactId === null ? "\u6CA1\u6709\u53EF\u5E94\u7528\u7684\u5DF2\u6279\u51C6\u66F4\u65B0\u3002\n" : "\u66F4\u65B0\u5B8C\u6210\uFF1A\u5DF2\u5E94\u7528 " + result.applied.length + " \u4E2A\u6761\u76EE\uFF0C\u8DF3\u8FC7 " + result.skipped.length + " \u4E2A\u3002\n");
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");
5575
6088
  return exitCode;
5576
6089
  } catch (error) {
5577
6090
  const exitCode = error instanceof UpdateWorkflowError ? error.exitCode : 1;
@@ -5601,7 +6114,7 @@ async function runUpdate(options, dependencies) {
5601
6114
 
5602
6115
  // src/commands/recovery.ts
5603
6116
  import { stat as stat5 } from "node:fs/promises";
5604
- import { join as join17 } from "node:path";
6117
+ import { join as join18 } from "node:path";
5605
6118
  async function exists5(path) {
5606
6119
  try {
5607
6120
  await stat5(path);
@@ -5661,7 +6174,7 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
5661
6174
  return 5;
5662
6175
  }
5663
6176
  }
5664
- const initialized = await exists5(join17(dependencies.cwd, ".harness", "project.yaml"));
6177
+ const initialized = await exists5(join18(dependencies.cwd, ".harness", "project.yaml"));
5665
6178
  if (!initialized || options.nonInteractive === true || explicitConfigure(options)) {
5666
6179
  return null;
5667
6180
  }
@@ -5704,8 +6217,8 @@ async function runRecoveryMenuIfApplicable(options, dependencies) {
5704
6217
  }
5705
6218
 
5706
6219
  // src/workflow-data/resolve.ts
5707
- import { cp, mkdir as mkdir5, readdir as readdir8, readFile as readFile16, rm as rm8, stat as stat6 } from "node:fs/promises";
5708
- import { dirname as dirname5, join as join18, relative as relative2 } from "node:path";
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";
5709
6222
  import { fileURLToPath } from "node:url";
5710
6223
  var WorkflowDataResolutionError = class extends Error {
5711
6224
  code;
@@ -5742,14 +6255,14 @@ async function listFilesRecursive(root, base = root) {
5742
6255
  const entries = await readdir8(root, { withFileTypes: true });
5743
6256
  const files = [];
5744
6257
  for (const entry of entries) {
5745
- const absolute = join18(root, entry.name);
6258
+ const absolute = join19(root, entry.name);
5746
6259
  if (entry.isDirectory()) {
5747
6260
  files.push(...await listFilesRecursive(absolute, base));
5748
6261
  continue;
5749
6262
  }
5750
6263
  if (!entry.isFile()) continue;
5751
6264
  const rel = relative2(base, absolute).replaceAll("\\", "/");
5752
- files.push({ path: rel, content: await readFile16(absolute, "utf8") });
6265
+ files.push({ path: rel, content: await readFile17(absolute, "utf8") });
5753
6266
  }
5754
6267
  return files;
5755
6268
  }
@@ -5762,7 +6275,7 @@ async function verifyWorkflowPackageIntegrity(resourcesRoot) {
5762
6275
  const manifest = await readWorkflowFamilyManifest(resourcesRoot);
5763
6276
  const expected = manifest.content_sha256;
5764
6277
  if (typeof expected !== "string" || expected.length === 0) return;
5765
- const harnessRoot = join18(resourcesRoot, "harness");
6278
+ const harnessRoot = join19(resourcesRoot, "harness");
5766
6279
  if (!await pathExists3(harnessRoot)) {
5767
6280
  throw new WorkflowDataResolutionError(
5768
6281
  "\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u7F3A\u5C11 harness/\uFF0C\u65E0\u6CD5\u6821\u9A8C SHA-256",
@@ -5784,37 +6297,37 @@ async function monorepoResourcesRoot() {
5784
6297
  const here = dirname5(fileURLToPath(import.meta.url));
5785
6298
  const candidates = [
5786
6299
  // TypeScript source: packages/cli/src/workflow-data -> packages/workflow-data-harness.
5787
- join18(here, "../../../workflow-data-harness"),
6300
+ join19(here, "../../../workflow-data-harness"),
5788
6301
  // Bundled CLI: packages/cli/dist -> packages/workflow-data-harness.
5789
- join18(here, "../../workflow-data-harness"),
6302
+ join19(here, "../../workflow-data-harness"),
5790
6303
  // Test/build layouts that preserve additional source directory levels.
5791
- join18(here, "../../../../packages/workflow-data-harness")
6304
+ join19(here, "../../../../packages/workflow-data-harness")
5792
6305
  ];
5793
6306
  for (const candidate of candidates) {
5794
- if (await pathExists3(join18(candidate, "harness", "manifests"))) return candidate;
6307
+ if (await pathExists3(join19(candidate, "harness", "manifests"))) return candidate;
5795
6308
  }
5796
6309
  return null;
5797
6310
  }
5798
6311
  async function siblingWorkflowPackage(cwd) {
5799
6312
  const here = dirname5(fileURLToPath(import.meta.url));
5800
6313
  const candidates = [
5801
- join18(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
6314
+ join19(cwd, "node_modules", "@hunter-harness", "workflow-harness"),
5802
6315
  // Published layout: node_modules/hunter-harness/dist/bin.js alongside the
5803
6316
  // scoped workflow package in node_modules/@hunter-harness/workflow-harness.
5804
- join18(here, "..", "..", "@hunter-harness", "workflow-harness"),
6317
+ join19(here, "..", "..", "@hunter-harness", "workflow-harness"),
5805
6318
  // Monorepo bundled layout: packages/cli/dist/bin.js.
5806
- join18(here, "..", "..", "workflow-data-harness")
6319
+ join19(here, "..", "..", "workflow-data-harness")
5807
6320
  ];
5808
6321
  for (const candidate of candidates) {
5809
- if (await pathExists3(join18(candidate, "harness", "manifests"))) return candidate;
6322
+ if (await pathExists3(join19(candidate, "harness", "manifests"))) return candidate;
5810
6323
  }
5811
6324
  return null;
5812
6325
  }
5813
6326
  async function readCachedNpmPackageVersion(cacheRoot) {
5814
- const manifestPath = join18(cacheRoot, "package.json");
6327
+ const manifestPath = join19(cacheRoot, "package.json");
5815
6328
  if (!await pathExists3(manifestPath)) return null;
5816
6329
  try {
5817
- const pkg = JSON.parse(await readFile16(manifestPath, "utf8"));
6330
+ const pkg = JSON.parse(await readFile17(manifestPath, "utf8"));
5818
6331
  return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : null;
5819
6332
  } catch {
5820
6333
  return null;
@@ -5837,12 +6350,12 @@ async function latestWorkflowCacheIsStale(cacheRoot, packageName, resolveManifes
5837
6350
  }
5838
6351
  async function materializeWorkflowPackageCache(cacheRoot, packageSpec, extract) {
5839
6352
  await mkdir5(cacheRoot, { recursive: true });
5840
- const staging = join18(cacheRoot, ".extract");
6353
+ const staging = join19(cacheRoot, ".extract");
5841
6354
  await rm8(staging, { recursive: true, force: true });
5842
6355
  await mkdir5(staging, { recursive: true });
5843
6356
  await extract(packageSpec, staging);
5844
- const packageDir = await pathExists3(join18(staging, "harness", "manifests")) ? staging : join18(staging, "package");
5845
- if (!await pathExists3(join18(packageDir, "harness", "manifests"))) {
6357
+ const packageDir = await pathExists3(join19(staging, "harness", "manifests")) ? staging : join19(staging, "package");
6358
+ if (!await pathExists3(join19(packageDir, "harness", "manifests"))) {
5846
6359
  throw new WorkflowDataResolutionError(
5847
6360
  "\u5DE5\u4F5C\u6D41\u6570\u636E\u5305\u5185\u5BB9\u65E0\u6548\uFF1A\u7F3A\u5C11 harness/manifests",
5848
6361
  "WORKFLOW_DATA_INVALID",
@@ -5871,8 +6384,8 @@ async function resolveWorkflowResourcesRoot(options, argv = []) {
5871
6384
  const packageName = workflowPackageName(family, options.env);
5872
6385
  const packageSpec = version === "latest" ? packageName : `${packageName}@${version}`;
5873
6386
  const cacheKey = packageSpec.replace("/", "+");
5874
- const cacheRoot = join18(options.cwd, ".harness", "cache", "workflow-packages", cacheKey);
5875
- 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"))) {
5876
6389
  if (version === "latest") {
5877
6390
  const stale = await latestWorkflowCacheIsStale(cacheRoot, packageName, options.pacoteManifest);
5878
6391
  if (stale) {
@@ -5918,9 +6431,9 @@ function describeWorkflowDataFetchFailure(error, packageSpec) {
5918
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}`;
5919
6432
  }
5920
6433
  async function readWorkflowFamilyManifest(resourcesRoot) {
5921
- const manifestPath = join18(resourcesRoot, "hunter-workflow-family.json");
6434
+ const manifestPath = join19(resourcesRoot, "hunter-workflow-family.json");
5922
6435
  if (!await pathExists3(manifestPath)) return {};
5923
- return JSON.parse(await readFile16(manifestPath, "utf8"));
6436
+ return JSON.parse(await readFile17(manifestPath, "utf8"));
5924
6437
  }
5925
6438
 
5926
6439
  // src/bin.ts
@@ -6011,7 +6524,15 @@ async function runCli(argv, overrides = {}) {
6011
6524
  dependencies
6012
6525
  );
6013
6526
  });
6014
- 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) => {
6015
6536
  exitCode = await runUpdate(
6016
6537
  { ...program.opts(), ...options },
6017
6538
  dependencies