clawt 3.1.3 → 3.2.0

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.
package/dist/index.js CHANGED
@@ -180,6 +180,8 @@ var VALIDATE_MESSAGES = {
180
180
  \u6682\u5B58\u533A = \u4E0A\u6B21\u5FEB\u7167\uFF0C\u5DE5\u4F5C\u76EE\u5F55 = \u6700\u65B0\u53D8\u66F4`,
181
181
  /** 增量 validate 降级为全量模式提示 */
182
182
  INCREMENTAL_VALIDATE_FALLBACK: "\u589E\u91CF\u5BF9\u6BD4\u5931\u8D25\uFF0C\u5DF2\u964D\u7EA7\u4E3A\u5168\u91CF\u6A21\u5F0F",
183
+ /** 增量 validate 检测到目标 worktree 无新变更 */
184
+ INCREMENTAL_VALIDATE_NO_CHANGES: (branch) => `\u5206\u652F ${branch} \u81EA\u4E0A\u6B21 validate \u4EE5\u6765\u6CA1\u6709\u65B0\u7684\u53D8\u66F4\uFF0C\u5DF2\u6062\u590D\u5230\u4E0A\u6B21\u9A8C\u8BC1\u72B6\u6001`,
183
185
  /** validate 状态已清理 */
184
186
  VALIDATE_CLEANED: (branch) => `\u2713 \u5206\u652F ${branch} \u7684 validate \u72B6\u6001\u5DF2\u6E05\u7406`,
185
187
  /** validate patch apply 失败,提示用户同步主分支 */
@@ -1672,6 +1674,9 @@ function getSnapshotPath(projectName, branchName) {
1672
1674
  function getSnapshotHeadPath(projectName, branchName) {
1673
1675
  return join6(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
1674
1676
  }
1677
+ function getSnapshotStagedPath(projectName, branchName) {
1678
+ return join6(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.staged`);
1679
+ }
1675
1680
  function hasSnapshot(projectName, branchName) {
1676
1681
  return existsSync8(getSnapshotPath(projectName, branchName));
1677
1682
  }
@@ -1684,23 +1689,28 @@ function getSnapshotModifiedTime(projectName, branchName) {
1684
1689
  function readSnapshot(projectName, branchName) {
1685
1690
  const snapshotPath = getSnapshotPath(projectName, branchName);
1686
1691
  const headPath = getSnapshotHeadPath(projectName, branchName);
1692
+ const stagedPath = getSnapshotStagedPath(projectName, branchName);
1687
1693
  logger.debug(`\u8BFB\u53D6 validate \u5FEB\u7167: ${snapshotPath}`);
1688
1694
  const treeHash = existsSync8(snapshotPath) ? readFileSync3(snapshotPath, "utf-8").trim() : "";
1689
1695
  const headCommitHash = existsSync8(headPath) ? readFileSync3(headPath, "utf-8").trim() : "";
1690
- return { treeHash, headCommitHash };
1696
+ const stagedTreeHash = existsSync8(stagedPath) ? readFileSync3(stagedPath, "utf-8").trim() : "";
1697
+ return { treeHash, headCommitHash, stagedTreeHash };
1691
1698
  }
1692
- function writeSnapshot(projectName, branchName, treeHash, headCommitHash) {
1699
+ function writeSnapshot(projectName, branchName, treeHash, headCommitHash, stagedTreeHash = "") {
1693
1700
  const snapshotPath = getSnapshotPath(projectName, branchName);
1694
1701
  const headPath = getSnapshotHeadPath(projectName, branchName);
1702
+ const stagedPath = getSnapshotStagedPath(projectName, branchName);
1695
1703
  const snapshotDir = join6(VALIDATE_SNAPSHOTS_DIR, projectName);
1696
1704
  ensureDir(snapshotDir);
1697
1705
  writeFileSync3(snapshotPath, treeHash, "utf-8");
1698
1706
  writeFileSync3(headPath, headCommitHash, "utf-8");
1699
- logger.info(`\u5DF2\u4FDD\u5B58 validate \u5FEB\u7167: ${snapshotPath}, ${headPath}`);
1707
+ writeFileSync3(stagedPath, stagedTreeHash, "utf-8");
1708
+ logger.info(`\u5DF2\u4FDD\u5B58 validate \u5FEB\u7167: ${snapshotPath}, ${headPath}, ${stagedPath}`);
1700
1709
  }
1701
1710
  function removeSnapshot(projectName, branchName) {
1702
1711
  const snapshotPath = getSnapshotPath(projectName, branchName);
1703
1712
  const headPath = getSnapshotHeadPath(projectName, branchName);
1713
+ const stagedPath = getSnapshotStagedPath(projectName, branchName);
1704
1714
  if (existsSync8(snapshotPath)) {
1705
1715
  unlinkSync(snapshotPath);
1706
1716
  logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${snapshotPath}`);
@@ -1709,6 +1719,10 @@ function removeSnapshot(projectName, branchName) {
1709
1719
  unlinkSync(headPath);
1710
1720
  logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${headPath}`);
1711
1721
  }
1722
+ if (existsSync8(stagedPath)) {
1723
+ unlinkSync(stagedPath);
1724
+ logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${stagedPath}`);
1725
+ }
1712
1726
  }
1713
1727
  function getProjectSnapshotBranches(projectName) {
1714
1728
  const projectDir = join6(VALIDATE_SNAPSHOTS_DIR, projectName);
@@ -2968,6 +2982,145 @@ async function checkForUpdates(currentVersion) {
2968
2982
  }
2969
2983
  }
2970
2984
 
2985
+ // src/utils/validate-runner.ts
2986
+ function executeSingleCommand(command, mainWorktreePath) {
2987
+ printInfo(MESSAGES.VALIDATE_RUN_START(command));
2988
+ printSeparator();
2989
+ const result = runCommandInherited(command, { cwd: mainWorktreePath });
2990
+ printSeparator();
2991
+ if (result.error) {
2992
+ printError(MESSAGES.VALIDATE_RUN_ERROR(command, result.error.message));
2993
+ return;
2994
+ }
2995
+ const exitCode = result.status ?? 1;
2996
+ if (exitCode === 0) {
2997
+ printSuccess(MESSAGES.VALIDATE_RUN_SUCCESS(command));
2998
+ } else {
2999
+ printError(MESSAGES.VALIDATE_RUN_FAILED(command, exitCode));
3000
+ }
3001
+ }
3002
+ function reportParallelResults(results) {
3003
+ printSeparator();
3004
+ const successCount = results.filter((r) => r.exitCode === 0 && !r.error).length;
3005
+ const failedCount = results.length - successCount;
3006
+ for (const result of results) {
3007
+ if (result.error) {
3008
+ printError(MESSAGES.VALIDATE_PARALLEL_CMD_ERROR(result.command, result.error));
3009
+ } else if (result.exitCode === 0) {
3010
+ printSuccess(MESSAGES.VALIDATE_PARALLEL_CMD_SUCCESS(result.command));
3011
+ } else {
3012
+ printError(MESSAGES.VALIDATE_PARALLEL_CMD_FAILED(result.command, result.exitCode));
3013
+ }
3014
+ }
3015
+ if (failedCount === 0) {
3016
+ printSuccess(MESSAGES.VALIDATE_PARALLEL_RUN_ALL_SUCCESS(results.length));
3017
+ } else {
3018
+ printError(MESSAGES.VALIDATE_PARALLEL_RUN_SUMMARY(successCount, failedCount));
3019
+ }
3020
+ }
3021
+ async function executeParallelCommands(commands, mainWorktreePath) {
3022
+ printInfo(MESSAGES.VALIDATE_PARALLEL_RUN_START(commands.length));
3023
+ for (let i = 0; i < commands.length; i++) {
3024
+ printInfo(MESSAGES.VALIDATE_PARALLEL_CMD_START(i + 1, commands.length, commands[i]));
3025
+ }
3026
+ printSeparator();
3027
+ const results = await runParallelCommands(commands, { cwd: mainWorktreePath });
3028
+ reportParallelResults(results);
3029
+ }
3030
+ async function executeRunCommand(command, mainWorktreePath) {
3031
+ printInfo("");
3032
+ const commands = parseParallelCommands(command);
3033
+ if (commands.length <= 1) {
3034
+ executeSingleCommand(commands[0] || command, mainWorktreePath);
3035
+ } else {
3036
+ await executeParallelCommands(commands, mainWorktreePath);
3037
+ }
3038
+ }
3039
+
3040
+ // src/utils/validate-core.ts
3041
+ function migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted) {
3042
+ let didTempCommit = false;
3043
+ try {
3044
+ if (hasUncommitted) {
3045
+ gitAddAll(targetWorktreePath);
3046
+ gitCommit("clawt:temp-commit-for-validate", targetWorktreePath);
3047
+ didTempCommit = true;
3048
+ }
3049
+ const patch = gitDiffBinaryAgainstBranch(branchName, mainWorktreePath);
3050
+ if (patch.length > 0) {
3051
+ try {
3052
+ gitApplyFromStdin(patch, mainWorktreePath);
3053
+ } catch (error) {
3054
+ logger.warn(`patch apply \u5931\u8D25: ${error}`);
3055
+ printWarning(MESSAGES.VALIDATE_PATCH_APPLY_FAILED(branchName));
3056
+ return { success: false };
3057
+ }
3058
+ }
3059
+ return { success: true };
3060
+ } finally {
3061
+ if (didTempCommit) {
3062
+ try {
3063
+ gitResetSoft(1, targetWorktreePath);
3064
+ } catch (error) {
3065
+ logger.error(`\u64A4\u9500\u4E34\u65F6 commit \u5931\u8D25: ${error}`);
3066
+ }
3067
+ try {
3068
+ gitRestoreStaged(targetWorktreePath);
3069
+ } catch (error) {
3070
+ logger.error(`\u6062\u590D\u6682\u5B58\u533A\u5931\u8D25: ${error}`);
3071
+ }
3072
+ }
3073
+ }
3074
+ }
3075
+ function computeCurrentTreeHash(mainWorktreePath) {
3076
+ gitAddAll(mainWorktreePath);
3077
+ const treeHash = gitWriteTree(mainWorktreePath);
3078
+ gitRestoreStaged(mainWorktreePath);
3079
+ return treeHash;
3080
+ }
3081
+ function saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName, stagedTreeHash = "") {
3082
+ gitAddAll(mainWorktreePath);
3083
+ const treeHash = gitWriteTree(mainWorktreePath);
3084
+ gitRestoreStaged(mainWorktreePath);
3085
+ const headCommitHash = getHeadCommitHash(mainWorktreePath);
3086
+ writeSnapshot(projectName, branchName, treeHash, headCommitHash, stagedTreeHash);
3087
+ return treeHash;
3088
+ }
3089
+ function loadOldSnapshotToStage(oldTreeHash, oldHeadCommitHash, currentHeadCommitHash, mainWorktreePath) {
3090
+ try {
3091
+ if (oldHeadCommitHash && oldHeadCommitHash !== currentHeadCommitHash) {
3092
+ const oldHeadTreeHash = getCommitTreeHash(oldHeadCommitHash, mainWorktreePath);
3093
+ const oldChangePatch = gitDiffTree(oldHeadTreeHash, oldTreeHash, mainWorktreePath);
3094
+ if (oldChangePatch.length > 0 && gitApplyCachedCheck(oldChangePatch, mainWorktreePath)) {
3095
+ gitApplyCachedFromStdin(oldChangePatch, mainWorktreePath);
3096
+ const stagedTreeHash = gitWriteTree(mainWorktreePath);
3097
+ return { success: true, stagedTreeHash };
3098
+ } else if (oldChangePatch.length > 0) {
3099
+ logger.warn("\u65E7\u53D8\u66F4 patch \u4E0E\u5F53\u524D HEAD \u51B2\u7A81\uFF0C\u964D\u7EA7\u4E3A\u5168\u91CF\u6A21\u5F0F");
3100
+ return { success: false, stagedTreeHash: "" };
3101
+ }
3102
+ return { success: true, stagedTreeHash: "" };
3103
+ } else {
3104
+ gitReadTree(oldTreeHash, mainWorktreePath);
3105
+ return { success: true, stagedTreeHash: oldTreeHash };
3106
+ }
3107
+ } catch (error) {
3108
+ logger.warn(`\u589E\u91CF read-tree \u5931\u8D25: ${error}`);
3109
+ return { success: false, stagedTreeHash: "" };
3110
+ }
3111
+ }
3112
+ function switchToValidateBranch(branchName, mainWorktreePath) {
3113
+ const validateBranchName = getValidateBranchName(branchName);
3114
+ if (!checkBranchExists(validateBranchName)) {
3115
+ throw new ClawtError(MESSAGES.VALIDATE_BRANCH_NOT_FOUND(validateBranchName, branchName));
3116
+ }
3117
+ const currentBranch = getCurrentBranch(mainWorktreePath);
3118
+ if (currentBranch !== validateBranchName) {
3119
+ gitCheckout(validateBranchName, mainWorktreePath);
3120
+ }
3121
+ return validateBranchName;
3122
+ }
3123
+
2971
3124
  // src/utils/interactive-panel.ts
2972
3125
  import { createInterface as createInterface2 } from "readline";
2973
3126
 
@@ -3934,40 +4087,6 @@ function registerValidateCommand(program2) {
3934
4087
  async function handleDirtyMainWorktree(mainWorktreePath) {
3935
4088
  await handleDirtyWorkingDir(mainWorktreePath);
3936
4089
  }
3937
- function migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted) {
3938
- let didTempCommit = false;
3939
- try {
3940
- if (hasUncommitted) {
3941
- gitAddAll(targetWorktreePath);
3942
- gitCommit("clawt:temp-commit-for-validate", targetWorktreePath);
3943
- didTempCommit = true;
3944
- }
3945
- const patch = gitDiffBinaryAgainstBranch(branchName, mainWorktreePath);
3946
- if (patch.length > 0) {
3947
- try {
3948
- gitApplyFromStdin(patch, mainWorktreePath);
3949
- } catch (error) {
3950
- logger.warn(`patch apply \u5931\u8D25: ${error}`);
3951
- printWarning(MESSAGES.VALIDATE_PATCH_APPLY_FAILED(branchName));
3952
- return { success: false };
3953
- }
3954
- }
3955
- return { success: true };
3956
- } finally {
3957
- if (didTempCommit) {
3958
- try {
3959
- gitResetSoft(1, targetWorktreePath);
3960
- } catch (error) {
3961
- logger.error(`\u64A4\u9500\u4E34\u65F6 commit \u5931\u8D25: ${error}`);
3962
- }
3963
- try {
3964
- gitRestoreStaged(targetWorktreePath);
3965
- } catch (error) {
3966
- logger.error(`\u6062\u590D\u6682\u5B58\u533A\u5931\u8D25: ${error}`);
3967
- }
3968
- }
3969
- }
3970
- }
3971
4090
  async function handlePatchApplyFailure(targetWorktreePath, branchName) {
3972
4091
  const confirmed = await confirmAction(MESSAGES.VALIDATE_CONFIRM_AUTO_SYNC(branchName));
3973
4092
  if (!confirmed) {
@@ -3977,14 +4096,6 @@ async function handlePatchApplyFailure(targetWorktreePath, branchName) {
3977
4096
  printInfo(MESSAGES.VALIDATE_AUTO_SYNC_START(branchName));
3978
4097
  const syncResult = await executeSyncForBranch(targetWorktreePath, branchName);
3979
4098
  }
3980
- function saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName) {
3981
- gitAddAll(mainWorktreePath);
3982
- const treeHash = gitWriteTree(mainWorktreePath);
3983
- gitRestoreStaged(mainWorktreePath);
3984
- const headCommitHash = getHeadCommitHash(mainWorktreePath);
3985
- writeSnapshot(projectName, branchName, treeHash, headCommitHash);
3986
- return treeHash;
3987
- }
3988
4099
  async function handleValidateClean(options) {
3989
4100
  validateMainWorktree();
3990
4101
  requireProjectConfig();
@@ -4013,11 +4124,7 @@ async function handleValidateClean(options) {
4013
4124
  printSuccess(MESSAGES.VALIDATE_CLEANED(branchName));
4014
4125
  }
4015
4126
  async function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
4016
- const validateBranchName = getValidateBranchName(branchName);
4017
- if (!checkBranchExists(validateBranchName)) {
4018
- throw new ClawtError(MESSAGES.VALIDATE_BRANCH_NOT_FOUND(validateBranchName, branchName));
4019
- }
4020
- gitCheckout(validateBranchName, mainWorktreePath);
4127
+ const validateBranchName = switchToValidateBranch(branchName, mainWorktreePath);
4021
4128
  const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
4022
4129
  if (!result.success) {
4023
4130
  await ensureOnMainWorkBranch(mainWorktreePath);
@@ -4028,102 +4135,42 @@ async function handleFirstValidate(targetWorktreePath, mainWorktreePath, project
4028
4135
  printSuccess(MESSAGES.VALIDATE_SUCCESS_WITH_BRANCH(branchName, validateBranchName));
4029
4136
  }
4030
4137
  async function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
4031
- const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash } = readSnapshot(projectName, branchName);
4138
+ const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash, stagedTreeHash: oldStagedTreeHash } = readSnapshot(projectName, branchName);
4032
4139
  if (!isWorkingDirClean(mainWorktreePath)) {
4033
4140
  gitResetHard(mainWorktreePath);
4034
4141
  gitCleanForce(mainWorktreePath);
4035
4142
  }
4036
- const validateBranchName = getValidateBranchName(branchName);
4037
- if (!checkBranchExists(validateBranchName)) {
4038
- throw new ClawtError(MESSAGES.VALIDATE_BRANCH_NOT_FOUND(validateBranchName, branchName));
4039
- }
4040
- const currentBranch = getCurrentBranch(mainWorktreePath);
4041
- if (currentBranch !== validateBranchName) {
4042
- gitCheckout(validateBranchName, mainWorktreePath);
4043
- }
4143
+ const validateBranchName = switchToValidateBranch(branchName, mainWorktreePath);
4044
4144
  const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
4045
4145
  if (!result.success) {
4046
4146
  await ensureOnMainWorkBranch(mainWorktreePath);
4047
4147
  await handlePatchApplyFailure(targetWorktreePath, branchName);
4048
4148
  return;
4049
4149
  }
4050
- saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
4051
- try {
4052
- const currentHeadCommitHash = getHeadCommitHash(mainWorktreePath);
4053
- if (oldHeadCommitHash && oldHeadCommitHash !== currentHeadCommitHash) {
4054
- const oldHeadTreeHash = getCommitTreeHash(oldHeadCommitHash, mainWorktreePath);
4055
- const oldChangePatch = gitDiffTree(oldHeadTreeHash, oldTreeHash, mainWorktreePath);
4056
- if (oldChangePatch.length > 0 && gitApplyCachedCheck(oldChangePatch, mainWorktreePath)) {
4057
- gitApplyCachedFromStdin(oldChangePatch, mainWorktreePath);
4058
- } else if (oldChangePatch.length > 0) {
4059
- logger.warn("\u65E7\u53D8\u66F4 patch \u4E0E\u5F53\u524D HEAD \u51B2\u7A81\uFF0C\u964D\u7EA7\u4E3A\u5168\u91CF\u6A21\u5F0F");
4060
- printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
4061
- printSuccess(MESSAGES.VALIDATE_SUCCESS_WITH_BRANCH(branchName, validateBranchName));
4062
- return;
4150
+ const newTreeHash = computeCurrentTreeHash(mainWorktreePath);
4151
+ const currentHeadCommitHash = getHeadCommitHash(mainWorktreePath);
4152
+ const hasNewChanges = newTreeHash !== oldTreeHash || oldHeadCommitHash && oldHeadCommitHash !== currentHeadCommitHash;
4153
+ if (!hasNewChanges) {
4154
+ if (oldStagedTreeHash) {
4155
+ try {
4156
+ gitReadTree(oldStagedTreeHash, mainWorktreePath);
4157
+ } catch (error) {
4158
+ logger.warn(`\u6062\u590D\u6682\u5B58\u533A\u5931\u8D25: ${error}`);
4063
4159
  }
4064
- } else {
4065
- gitReadTree(oldTreeHash, mainWorktreePath);
4066
4160
  }
4067
- } catch (error) {
4068
- logger.warn(`\u589E\u91CF read-tree \u5931\u8D25: ${error}`);
4069
- printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
4161
+ printInfo(MESSAGES.INCREMENTAL_VALIDATE_NO_CHANGES(branchName));
4070
4162
  printSuccess(MESSAGES.VALIDATE_SUCCESS_WITH_BRANCH(branchName, validateBranchName));
4071
4163
  return;
4072
4164
  }
4073
- printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
4074
- }
4075
- function executeSingleCommand(command, mainWorktreePath) {
4076
- printInfo(MESSAGES.VALIDATE_RUN_START(command));
4077
- printSeparator();
4078
- const result = runCommandInherited(command, { cwd: mainWorktreePath });
4079
- printSeparator();
4080
- if (result.error) {
4081
- printError(MESSAGES.VALIDATE_RUN_ERROR(command, result.error.message));
4165
+ const stageResult = loadOldSnapshotToStage(oldTreeHash, oldHeadCommitHash, currentHeadCommitHash, mainWorktreePath);
4166
+ if (!stageResult.success) {
4167
+ printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
4168
+ writeSnapshot(projectName, branchName, newTreeHash, currentHeadCommitHash, "");
4169
+ printSuccess(MESSAGES.VALIDATE_SUCCESS_WITH_BRANCH(branchName, validateBranchName));
4082
4170
  return;
4083
4171
  }
4084
- const exitCode = result.status ?? 1;
4085
- if (exitCode === 0) {
4086
- printSuccess(MESSAGES.VALIDATE_RUN_SUCCESS(command));
4087
- } else {
4088
- printError(MESSAGES.VALIDATE_RUN_FAILED(command, exitCode));
4089
- }
4090
- }
4091
- function reportParallelResults(results) {
4092
- printSeparator();
4093
- const successCount = results.filter((r) => r.exitCode === 0 && !r.error).length;
4094
- const failedCount = results.length - successCount;
4095
- for (const result of results) {
4096
- if (result.error) {
4097
- printError(MESSAGES.VALIDATE_PARALLEL_CMD_ERROR(result.command, result.error));
4098
- } else if (result.exitCode === 0) {
4099
- printSuccess(MESSAGES.VALIDATE_PARALLEL_CMD_SUCCESS(result.command));
4100
- } else {
4101
- printError(MESSAGES.VALIDATE_PARALLEL_CMD_FAILED(result.command, result.exitCode));
4102
- }
4103
- }
4104
- if (failedCount === 0) {
4105
- printSuccess(MESSAGES.VALIDATE_PARALLEL_RUN_ALL_SUCCESS(results.length));
4106
- } else {
4107
- printError(MESSAGES.VALIDATE_PARALLEL_RUN_SUMMARY(successCount, failedCount));
4108
- }
4109
- }
4110
- async function executeParallelCommands(commands, mainWorktreePath) {
4111
- printInfo(MESSAGES.VALIDATE_PARALLEL_RUN_START(commands.length));
4112
- for (let i = 0; i < commands.length; i++) {
4113
- printInfo(MESSAGES.VALIDATE_PARALLEL_CMD_START(i + 1, commands.length, commands[i]));
4114
- }
4115
- printSeparator();
4116
- const results = await runParallelCommands(commands, { cwd: mainWorktreePath });
4117
- reportParallelResults(results);
4118
- }
4119
- async function executeRunCommand(command, mainWorktreePath) {
4120
- printInfo("");
4121
- const commands = parseParallelCommands(command);
4122
- if (commands.length <= 1) {
4123
- executeSingleCommand(commands[0] || command, mainWorktreePath);
4124
- } else {
4125
- await executeParallelCommands(commands, mainWorktreePath);
4126
- }
4172
+ writeSnapshot(projectName, branchName, newTreeHash, currentHeadCommitHash, stageResult.stagedTreeHash);
4173
+ printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
4127
4174
  }
4128
4175
  function resolveRunCommand(optionRun) {
4129
4176
  if (optionRun) {
@@ -171,6 +171,8 @@ var VALIDATE_MESSAGES = {
171
171
  \u6682\u5B58\u533A = \u4E0A\u6B21\u5FEB\u7167\uFF0C\u5DE5\u4F5C\u76EE\u5F55 = \u6700\u65B0\u53D8\u66F4`,
172
172
  /** 增量 validate 降级为全量模式提示 */
173
173
  INCREMENTAL_VALIDATE_FALLBACK: "\u589E\u91CF\u5BF9\u6BD4\u5931\u8D25\uFF0C\u5DF2\u964D\u7EA7\u4E3A\u5168\u91CF\u6A21\u5F0F",
174
+ /** 增量 validate 检测到目标 worktree 无新变更 */
175
+ INCREMENTAL_VALIDATE_NO_CHANGES: (branch) => `\u5206\u652F ${branch} \u81EA\u4E0A\u6B21 validate \u4EE5\u6765\u6CA1\u6709\u65B0\u7684\u53D8\u66F4\uFF0C\u5DF2\u6062\u590D\u5230\u4E0A\u6B21\u9A8C\u8BC1\u72B6\u6001`,
174
176
  /** validate 状态已清理 */
175
177
  VALIDATE_CLEANED: (branch) => `\u2713 \u5206\u652F ${branch} \u7684 validate \u72B6\u6001\u5DF2\u6E05\u7406`,
176
178
  /** validate patch apply 失败,提示用户同步主分支 */
package/docs/spec.md CHANGED
@@ -254,6 +254,7 @@ async function interactiveConfigEditor<T extends object>(
254
254
  │ └── <project-name>/ # 以项目名分组
255
255
  │ ├── <branchName>.tree # 每个分支一个 tree hash 快照文件(存储 git tree 对象的 hash)
256
256
  │ ├── <branchName>.head # 每个分支一个 HEAD commit hash 快照文件(存储快照时验证分支的 HEAD commit hash)
257
+ │ ├── <branchName>.staged # 每个分支一个 staged tree hash 快照文件(存储 validate 结束时暂存区对应的 tree hash,用于无变更时恢复)
257
258
  │ └── ...
258
259
  ├── projects/<project-name>/ # 项目级配置目录
259
260
  │ └── config.json # 项目级配置(含 clawtMainWorkBranch)
package/docs/validate.md CHANGED
@@ -30,7 +30,7 @@ validate 不再在主工作分支上直接 apply patch,而是先切换到目
30
30
 
31
31
  **快照机制:**
32
32
 
33
- validate 命令引入了**快照(snapshot)机制**来支持增量对比。每次 validate 执行成功后,会将当前全量变更通过 `git write-tree` 保存为 git tree 对象,并将 tree hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`),同时将验证分支的 HEAD commit hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.head`),用于增量 validate 时对齐基准。当再次执行 validate 时,如果验证分支 HEAD 未变化(正常情况),通过 `git read-tree` 将上次快照的 tree 对象载入暂存区;如果验证分支 HEAD 已变化(sync 后重建了验证分支),则将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上,避免新旧 tree 基准不同导致 diff 混入 HEAD 变化的内容。最终用户可通过 `git diff` 查看两次 validate 之间的增量差异。
33
+ validate 命令引入了**快照(snapshot)机制**来支持增量对比。每次 validate 执行成功后,会将当前全量变更通过 `git write-tree` 保存为 git tree 对象,并将 tree hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`),同时将验证分支的 HEAD commit hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.head`),以及 validate 结束时暂存区对应的 tree hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.staged`),用于增量 validate 时对齐基准和无变更恢复。当再次执行 validate 时,先计算当前变更的 tree hash 与旧快照对比:如果没有新变更(tree hash 和 HEAD 均未变化),直接通过 `git read-tree` 恢复上次 validate 结束时的暂存区状态,跳过后续步骤;如果有新变更,则继续执行暂存区载入流程——如果验证分支 HEAD 未变化(正常情况),通过 `git read-tree` 将上次快照的 tree 对象载入暂存区;如果验证分支 HEAD 已变化(sync 后重建了验证分支),则将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上,避免新旧 tree 基准不同导致 diff 混入 HEAD 变化的内容。最终用户可通过 `git diff` 查看两次 validate 之间的增量差异。
34
34
 
35
35
  **运行流程:**
36
36
 
@@ -289,7 +289,7 @@ clawt validate -b feature-scheme-1 -r "pnpm test & pnpm build"
289
289
 
290
290
  ##### 步骤 1:读取旧快照
291
291
 
292
- 在清空主 worktree 之前,读取上次保存的快照 tree hash 及当时的 HEAD commit hash
292
+ 在清空主 worktree 之前,读取上次保存的快照 tree hash、当时的 HEAD commit hash 和暂存区 tree hash(`stagedTreeHash`)。
293
293
 
294
294
  ##### 步骤 2:确保主 worktree 干净
295
295
 
@@ -307,9 +307,27 @@ git checkout clawt-validate-<branchName>
307
307
 
308
308
  通过 patch 方式从目标分支获取最新全量变更(流程同首次 validate 的步骤 4)。如果 patch apply 失败,同样进入自动 sync 交互流程(见首次 validate 的 [patch apply 失败后的自动 sync 流程](#patch-apply-失败后的自动-sync-流程)),validate 流程提前结束。
309
309
 
310
- ##### 步骤 5:保存最新快照为 git tree 对象
310
+ ##### 步骤 5:检测是否有新变更
311
311
 
312
- 将最新全量变更保存为新的 tree 对象(覆盖旧快照),同时记录验证分支的 HEAD commit hash(流程同首次 validate 的步骤 5)。
312
+ 计算当前工作目录变更的 tree hash(`git add . → git write-tree → git restore --staged .`),并与旧快照的 tree hash 及 HEAD commit hash 对比:
313
+
314
+ ```bash
315
+ # 计算当前变更的 tree hash
316
+ git add .
317
+ git write-tree # → newTreeHash
318
+ git restore --staged .
319
+
320
+ # 获取当前 HEAD
321
+ git rev-parse HEAD # → currentHeadCommitHash
322
+
323
+ # 判断是否有新变更
324
+ hasNewChanges = (newTreeHash !== oldTreeHash) || (oldHeadCommitHash !== currentHeadCommitHash)
325
+ ```
326
+
327
+ - **无新变更**(tree hash 和 HEAD 均未变化)→ 不更新快照,直接通过 `git read-tree <oldStagedTreeHash>` 恢复上次 validate 结束时的暂存区状态,输出提示后返回
328
+ - **有新变更** → 继续步骤 6
329
+
330
+ > 无变更检测避免了重复 validate 时不必要的快照更新和暂存区重载操作。恢复上次暂存区状态后,用户看到的 `git diff` 结果与上次 validate 结束时完全一致。
313
331
 
314
332
  ##### 步骤 6:将旧变更状态载入暂存区
315
333
 
@@ -323,8 +341,8 @@ git checkout clawt-validate-<branchName>
323
341
  git read-tree <旧 tree hash>
324
342
  ```
325
343
 
326
- - **读取成功** → 结果:暂存区=上次快照,工作目录=最新全量变更(用户可通过 `git diff` 查看增量差异)
327
- - **读取失败**(tree 对象可能被 git gc 回收)→ 降级为全量模式,暂存区保持为空,等同于首次 validate 的结果
344
+ - **读取成功** → 记录 `newStagedTreeHash = oldTreeHash`,结果:暂存区=上次快照,工作目录=最新全量变更(用户可通过 `git diff` 查看增量差异)
345
+ - **读取失败**(tree 对象可能被 git gc 回收)→ 降级为全量模式,写入快照(`stagedTreeHash` 为空)后返回,暂存区保持为空,等同于首次 validate 的结果
328
346
 
329
347
  > 这是最常见的路径。相比重构前,正常情况不再需要处理 HEAD 变化的复杂逻辑,代码路径更简单、更可靠。
330
348
 
@@ -344,25 +362,48 @@ git apply --cached --check < patch
344
362
 
345
363
  # 无冲突:apply --cached 到当前 HEAD 暂存区
346
364
  git apply --cached < patch
365
+
366
+ # 记录暂存区的 tree hash
367
+ git write-tree # → newStagedTreeHash
347
368
  ```
348
369
 
349
370
  - **patch 为空**(旧变更为空)→ 暂存区保持干净
350
- - **无冲突** → apply --cached 到当前 HEAD 暂存区,结果与正常情况一致
351
- - **有冲突** → 降级为全量模式(暂存区保持为空),等同于首次 validate 的结果
371
+ - **无冲突** → apply --cached 到当前 HEAD 暂存区,通过 `git write-tree` 记录 `newStagedTreeHash`,结果与正常情况一致
372
+ - **有冲突** → 降级为全量模式(暂存区保持为空),写入快照(`stagedTreeHash` 为空)后返回
352
373
 
353
- ##### 步骤 7:输出成功提示
374
+ ##### 步骤 7:写入新快照
375
+
376
+ 将步骤 5 计算的 `newTreeHash`、当前 HEAD commit hash 和步骤 6 记录的 `newStagedTreeHash` 写入快照文件,供下次 validate 使用:
377
+
378
+ ```bash
379
+ # 写入 ~/.clawt/validate-snapshots/<project>/<branchName>.tree
380
+ echo <newTreeHash>
381
+
382
+ # 写入 ~/.clawt/validate-snapshots/<project>/<branchName>.head
383
+ echo <currentHeadCommitHash>
384
+
385
+ # 写入 ~/.clawt/validate-snapshots/<project>/<branchName>.staged
386
+ echo <newStagedTreeHash>
387
+ ```
388
+
389
+ > `stagedTreeHash` 记录了 validate 结束时暂存区的完整状态。下次 validate 如果检测到无新变更,可直接通过此值恢复暂存区,避免重复执行 read-tree 或 apply --cached 流程。
390
+
391
+ ##### 步骤 8:输出成功提示
354
392
 
355
393
  ```
356
394
  # 增量模式成功
357
395
  ✓ 已将分支 feature-scheme-1 的最新变更应用到主 worktree(增量模式)
358
396
  暂存区 = 上次快照,工作目录 = 最新变更
359
397
 
398
+ # 增量无变更
399
+ 分支 feature-scheme-1 自上次 validate 以来没有新的变更,已恢复到上次验证状态
400
+
360
401
  # 增量降级为全量
361
402
  ✓ 已切换到验证分支 clawt-validate-feature-scheme-1 并应用分支 feature-scheme-1 的变更
362
403
  可以开始验证了
363
404
  ```
364
405
 
365
- ##### 步骤 8:执行 `--run` 命令(可选)
406
+ ##### 步骤 9:执行 `--run` 命令(可选)
366
407
 
367
408
  与首次 validate 的步骤 7 相同,增量 validate 成功后也会执行 `-r, --run` 指定的命令(或从项目配置 `validateRunCommand` 读取的默认命令)。
368
409
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "3.1.3",
3
+ "version": "3.2.0",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",