hamster-wheel-cli 0.1.0 → 0.2.0-beta.1

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/cli.js CHANGED
@@ -35,6 +35,7 @@ __export(cli_exports, {
35
35
  });
36
36
  module.exports = __toCommonJS(cli_exports);
37
37
  var import_node_child_process = require("child_process");
38
+ var import_fs_extra12 = __toESM(require("fs-extra"));
38
39
  var import_commander = require("commander");
39
40
 
40
41
  // src/config.ts
@@ -201,7 +202,8 @@ function buildPrConfig(options) {
201
202
  title: options.prTitle,
202
203
  bodyPath: options.prBody,
203
204
  draft: options.draft,
204
- reviewers: options.reviewers
205
+ reviewers: options.reviewers,
206
+ autoMerge: options.autoMerge
205
207
  };
206
208
  }
207
209
  function buildWebhookConfig(options) {
@@ -236,7 +238,8 @@ function buildLoopConfig(options, cwd) {
236
238
  runE2e: options.runE2e,
237
239
  autoCommit: options.autoCommit,
238
240
  autoPush: options.autoPush,
239
- skipInstall: options.skipInstall
241
+ skipInstall: options.skipInstall,
242
+ skipQuality: options.skipQuality
240
243
  };
241
244
  }
242
245
  function defaultNotesPath() {
@@ -367,12 +370,79 @@ function parseTomlString(raw) {
367
370
  }
368
371
  return null;
369
372
  }
373
+ function parseTomlKeyValue(line) {
374
+ const equalIndex = findUnquotedIndex(line, "=");
375
+ if (equalIndex <= 0) return null;
376
+ const key = line.slice(0, equalIndex).trim();
377
+ const valuePart = line.slice(equalIndex + 1).trim();
378
+ if (!key || !valuePart) return null;
379
+ const parsedValue = parseTomlString(valuePart);
380
+ if (parsedValue === null) return null;
381
+ return { key, value: parsedValue };
382
+ }
370
383
  function normalizeShortcutName(name) {
371
384
  const trimmed = name.trim();
372
385
  if (!trimmed) return null;
373
386
  if (/\s/.test(trimmed)) return null;
374
387
  return trimmed;
375
388
  }
389
+ function normalizeAliasName(name) {
390
+ return normalizeShortcutName(name);
391
+ }
392
+ function formatTomlString(value) {
393
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
394
+ return `"${escaped}"`;
395
+ }
396
+ function updateAliasContent(content, name, command) {
397
+ const lines = content.split(/\r?\n/);
398
+ const entryLine = `${name} = ${formatTomlString(command)}`;
399
+ let currentSection = null;
400
+ let aliasStart = -1;
401
+ let aliasEnd = lines.length;
402
+ for (let i = 0; i < lines.length; i += 1) {
403
+ const match = /^\s*\[(.+?)\]\s*$/.exec(lines[i]);
404
+ if (!match) continue;
405
+ if (currentSection === "alias" && aliasStart >= 0 && aliasEnd === lines.length) {
406
+ aliasEnd = i;
407
+ }
408
+ currentSection = match[1].trim();
409
+ if (currentSection === "alias") {
410
+ aliasStart = i;
411
+ }
412
+ }
413
+ if (aliasStart < 0) {
414
+ const trimmed = content.trimEnd();
415
+ const prefix = trimmed.length > 0 ? `${trimmed}
416
+
417
+ ` : "";
418
+ return `${prefix}[alias]
419
+ ${entryLine}
420
+ `;
421
+ }
422
+ let replaced = false;
423
+ for (let i = aliasStart + 1; i < aliasEnd; i += 1) {
424
+ const parsed = parseTomlKeyValue(stripTomlComment(lines[i]).trim());
425
+ if (!parsed) continue;
426
+ if (parsed.key === name) {
427
+ lines[i] = entryLine;
428
+ replaced = true;
429
+ break;
430
+ }
431
+ }
432
+ if (!replaced) {
433
+ lines.splice(aliasEnd, 0, entryLine);
434
+ }
435
+ const output = lines.join("\n");
436
+ return output.endsWith("\n") ? output : `${output}
437
+ `;
438
+ }
439
+ async function upsertAliasEntry(name, command, filePath = getGlobalConfigPath()) {
440
+ const exists = await import_fs_extra2.default.pathExists(filePath);
441
+ const content = exists ? await import_fs_extra2.default.readFile(filePath, "utf8") : "";
442
+ const nextContent = updateAliasContent(content, name, command);
443
+ await import_fs_extra2.default.mkdirp(import_node_path3.default.dirname(filePath));
444
+ await import_fs_extra2.default.writeFile(filePath, nextContent, "utf8");
445
+ }
376
446
  function parseGlobalConfig(content) {
377
447
  const lines = content.split(/\r?\n/);
378
448
  let currentSection = null;
@@ -386,14 +456,9 @@ function parseGlobalConfig(content) {
386
456
  continue;
387
457
  }
388
458
  if (currentSection !== "shortcut") continue;
389
- const equalIndex = findUnquotedIndex(line, "=");
390
- if (equalIndex <= 0) continue;
391
- const key = line.slice(0, equalIndex).trim();
392
- const valuePart = line.slice(equalIndex + 1).trim();
393
- if (!key || !valuePart) continue;
394
- const parsedValue = parseTomlString(valuePart);
395
- if (parsedValue === null) continue;
396
- shortcut[key] = parsedValue;
459
+ const parsed = parseTomlKeyValue(line);
460
+ if (!parsed) continue;
461
+ shortcut[parsed.key] = parsed.value;
397
462
  }
398
463
  const name = normalizeShortcutName(shortcut.name ?? "");
399
464
  const command = (shortcut.command ?? "").trim();
@@ -407,6 +472,43 @@ function parseGlobalConfig(content) {
407
472
  }
408
473
  };
409
474
  }
475
+ function parseAliasEntries(content) {
476
+ const lines = content.split(/\r?\n/);
477
+ let currentSection = null;
478
+ const entries = [];
479
+ const names = /* @__PURE__ */ new Set();
480
+ for (const rawLine of lines) {
481
+ const line = stripTomlComment(rawLine).trim();
482
+ if (!line) continue;
483
+ const sectionMatch = /^\[(.+)\]$/.exec(line);
484
+ if (sectionMatch) {
485
+ currentSection = sectionMatch[1].trim();
486
+ continue;
487
+ }
488
+ if (currentSection !== "alias") continue;
489
+ const parsed = parseTomlKeyValue(line);
490
+ if (!parsed) continue;
491
+ const name = normalizeShortcutName(parsed.key);
492
+ const command = parsed.value.trim();
493
+ if (!name || !command) continue;
494
+ if (names.has(name)) continue;
495
+ names.add(name);
496
+ entries.push({
497
+ name,
498
+ command,
499
+ source: "alias"
500
+ });
501
+ }
502
+ const shortcut = parseGlobalConfig(content).shortcut;
503
+ if (shortcut && !names.has(shortcut.name)) {
504
+ entries.push({
505
+ name: shortcut.name,
506
+ command: shortcut.command,
507
+ source: "shortcut"
508
+ });
509
+ }
510
+ return entries;
511
+ }
410
512
  async function loadGlobalConfig(logger) {
411
513
  const filePath = getGlobalConfigPath();
412
514
  const exists = await import_fs_extra2.default.pathExists(filePath);
@@ -669,9 +771,10 @@ async function commitAll(message, cwd, logger) {
669
771
  });
670
772
  if (commit.exitCode !== 0) {
671
773
  logger.warn(`git commit \u8DF3\u8FC7\u6216\u5931\u8D25: ${commit.stderr}`);
672
- return;
774
+ return false;
673
775
  }
674
776
  logger.success("\u5DF2\u63D0\u4EA4\u5F53\u524D\u53D8\u66F4");
777
+ return true;
675
778
  }
676
779
  async function pushBranch(branchName, cwd, logger) {
677
780
  const push = await runCommand("git", ["push", "-u", "origin", branchName], {
@@ -709,7 +812,30 @@ async function removeWorktree(worktreePath, repoRoot, logger) {
709
812
  function generateBranchName() {
710
813
  const now = /* @__PURE__ */ new Date();
711
814
  const stamp = `${now.getFullYear()}${(now.getMonth() + 1).toString().padStart(2, "0")}${now.getDate().toString().padStart(2, "0")}-${now.getHours().toString().padStart(2, "0")}${now.getMinutes().toString().padStart(2, "0")}`;
712
- return `wheel-aii/${stamp}`;
815
+ return `wheel-ai/${stamp}`;
816
+ }
817
+ function guessBranchType(task) {
818
+ const text = task.toLowerCase();
819
+ if (/fix|bug|修复|错误|异常|问题/.test(text)) return "fix";
820
+ if (/docs|readme|changelog|文档/.test(text)) return "docs";
821
+ if (/test|e2e|单测|测试/.test(text)) return "test";
822
+ if (/refactor|重构/.test(text)) return "refactor";
823
+ if (/chore|构建|依赖|配置/.test(text)) return "chore";
824
+ return "feat";
825
+ }
826
+ function slugifyTask(task) {
827
+ const slug = task.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
828
+ return slug.slice(0, 40);
829
+ }
830
+ function buildTimestampSlug(now) {
831
+ const stamp = `${now.getFullYear()}${(now.getMonth() + 1).toString().padStart(2, "0")}${now.getDate().toString().padStart(2, "0")}-${now.getHours().toString().padStart(2, "0")}${now.getMinutes().toString().padStart(2, "0")}`;
832
+ return `auto-${stamp}`;
833
+ }
834
+ function generateBranchNameFromTask(task, now = /* @__PURE__ */ new Date()) {
835
+ const slug = slugifyTask(task);
836
+ const type = guessBranchType(task);
837
+ const suffix = slug || buildTimestampSlug(now);
838
+ return `${type}/${suffix}`;
713
839
  }
714
840
 
715
841
  // src/logs.ts
@@ -797,8 +923,192 @@ async function removeCurrentRegistry(logFile) {
797
923
  await writeJsonFile(getCurrentRegistryPath(), registry);
798
924
  }
799
925
 
800
- // src/logs-viewer.ts
926
+ // src/alias-viewer.ts
801
927
  var import_fs_extra4 = __toESM(require("fs-extra"));
928
+ function getTerminalSize() {
929
+ const rows = process.stdout.rows ?? 24;
930
+ const columns = process.stdout.columns ?? 80;
931
+ return { rows, columns };
932
+ }
933
+ function truncateLine(line, width) {
934
+ if (width <= 0) return "";
935
+ if (line.length <= width) return line;
936
+ return line.slice(0, width);
937
+ }
938
+ function getPageSize(rows) {
939
+ return Math.max(1, rows - 2);
940
+ }
941
+ function buildAliasLabel(entry) {
942
+ if (entry.source === "shortcut") {
943
+ return `${entry.name}\uFF08shortcut\uFF09`;
944
+ }
945
+ return entry.name;
946
+ }
947
+ function buildHeader(state, columns) {
948
+ const total = state.aliases.length;
949
+ const title = `\u522B\u540D\u5217\u8868\uFF08${total} \u6761\uFF09\uFF5C\u2191/\u2193 \u9009\u62E9 q \u9000\u51FA`;
950
+ return truncateLine(title, columns);
951
+ }
952
+ function buildStatus(state, columns) {
953
+ if (state.aliases.length === 0) {
954
+ if (state.lastError) {
955
+ return truncateLine(`\u8BFB\u53D6\u5931\u8D25\uFF1A${state.lastError}`, columns);
956
+ }
957
+ if (state.missingConfig) {
958
+ return truncateLine(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF1A${getGlobalConfigPath()}`, columns);
959
+ }
960
+ return truncateLine("\u672A\u53D1\u73B0 alias \u914D\u7F6E", columns);
961
+ }
962
+ const entry = state.aliases[state.selectedIndex];
963
+ const sourceText = entry.source === "shortcut" ? "\uFF08shortcut\uFF09" : "";
964
+ return truncateLine(`\u547D\u4EE4${sourceText}\uFF1A${entry.command}`, columns);
965
+ }
966
+ function buildListLine(entry, selected, columns) {
967
+ const marker = selected ? ">" : " ";
968
+ return truncateLine(`${marker} ${buildAliasLabel(entry)}`, columns);
969
+ }
970
+ function ensureListOffset(state, pageSize) {
971
+ const total = state.aliases.length;
972
+ if (total === 0) {
973
+ state.listOffset = 0;
974
+ state.selectedIndex = 0;
975
+ return;
976
+ }
977
+ const maxOffset = Math.max(0, total - pageSize);
978
+ if (state.selectedIndex < state.listOffset) {
979
+ state.listOffset = state.selectedIndex;
980
+ }
981
+ if (state.selectedIndex >= state.listOffset + pageSize) {
982
+ state.listOffset = state.selectedIndex - pageSize + 1;
983
+ }
984
+ state.listOffset = Math.min(Math.max(state.listOffset, 0), maxOffset);
985
+ }
986
+ function render(state) {
987
+ const { rows, columns } = getTerminalSize();
988
+ const pageSize = getPageSize(rows);
989
+ const header = buildHeader(state, columns);
990
+ ensureListOffset(state, pageSize);
991
+ if (state.aliases.length === 0) {
992
+ const filler = Array.from({ length: pageSize }, () => "");
993
+ const status2 = buildStatus(state, columns);
994
+ const content2 = [header, ...filler, status2].join("\n");
995
+ process.stdout.write(`\x1B[2J\x1B[H${content2}`);
996
+ return;
997
+ }
998
+ const start = state.listOffset;
999
+ const slice = state.aliases.slice(start, start + pageSize);
1000
+ const lines = slice.map((entry, index) => {
1001
+ const selected = start + index === state.selectedIndex;
1002
+ return buildListLine(entry, selected, columns);
1003
+ });
1004
+ while (lines.length < pageSize) {
1005
+ lines.push("");
1006
+ }
1007
+ const status = buildStatus(state, columns);
1008
+ const content = [header, ...lines, status].join("\n");
1009
+ process.stdout.write(`\x1B[2J\x1B[H${content}`);
1010
+ }
1011
+ function shouldExit(input) {
1012
+ if (input === "") return true;
1013
+ if (input.toLowerCase() === "q") return true;
1014
+ return false;
1015
+ }
1016
+ function isArrowUp(input) {
1017
+ return input.includes("\x1B[A");
1018
+ }
1019
+ function isArrowDown(input) {
1020
+ return input.includes("\x1B[B");
1021
+ }
1022
+ function setupCleanup(cleanup) {
1023
+ const exitHandler = () => {
1024
+ cleanup();
1025
+ };
1026
+ const signalHandler = () => {
1027
+ cleanup();
1028
+ process.exit(0);
1029
+ };
1030
+ process.on("SIGINT", signalHandler);
1031
+ process.on("SIGTERM", signalHandler);
1032
+ process.on("exit", exitHandler);
1033
+ }
1034
+ function clampIndex(value, total) {
1035
+ if (total <= 0) return 0;
1036
+ return Math.min(Math.max(value, 0), total - 1);
1037
+ }
1038
+ async function runAliasViewer() {
1039
+ if (!process.stdout.isTTY || !process.stdin.isTTY) {
1040
+ console.log("\u5F53\u524D\u7EC8\u7AEF\u4E0D\u652F\u6301\u4EA4\u4E92\u5F0F alias\u3002");
1041
+ return;
1042
+ }
1043
+ const state = {
1044
+ aliases: [],
1045
+ selectedIndex: 0,
1046
+ listOffset: 0,
1047
+ missingConfig: false
1048
+ };
1049
+ let cleaned = false;
1050
+ const cleanup = () => {
1051
+ if (cleaned) return;
1052
+ cleaned = true;
1053
+ if (process.stdin.isTTY) {
1054
+ process.stdin.setRawMode(false);
1055
+ process.stdin.pause();
1056
+ }
1057
+ process.stdout.write("\x1B[?25h");
1058
+ };
1059
+ setupCleanup(cleanup);
1060
+ process.stdout.write("\x1B[?25l");
1061
+ process.stdin.setRawMode(true);
1062
+ process.stdin.resume();
1063
+ const loadAliases = async () => {
1064
+ const filePath = getGlobalConfigPath();
1065
+ const exists = await import_fs_extra4.default.pathExists(filePath);
1066
+ if (!exists) {
1067
+ state.aliases = [];
1068
+ state.selectedIndex = 0;
1069
+ state.lastError = void 0;
1070
+ state.missingConfig = true;
1071
+ return;
1072
+ }
1073
+ try {
1074
+ const content = await import_fs_extra4.default.readFile(filePath, "utf8");
1075
+ state.aliases = parseAliasEntries(content);
1076
+ state.selectedIndex = clampIndex(state.selectedIndex, state.aliases.length);
1077
+ state.lastError = void 0;
1078
+ state.missingConfig = false;
1079
+ } catch (error) {
1080
+ const message = error instanceof Error ? error.message : String(error);
1081
+ state.aliases = [];
1082
+ state.selectedIndex = 0;
1083
+ state.lastError = message;
1084
+ state.missingConfig = false;
1085
+ }
1086
+ };
1087
+ await loadAliases();
1088
+ render(state);
1089
+ process.stdin.on("data", (data) => {
1090
+ const input = data.toString("utf8");
1091
+ if (shouldExit(input)) {
1092
+ cleanup();
1093
+ process.exit(0);
1094
+ }
1095
+ if (isArrowUp(input)) {
1096
+ state.selectedIndex = clampIndex(state.selectedIndex - 1, state.aliases.length);
1097
+ render(state);
1098
+ return;
1099
+ }
1100
+ if (isArrowDown(input)) {
1101
+ state.selectedIndex = clampIndex(state.selectedIndex + 1, state.aliases.length);
1102
+ render(state);
1103
+ }
1104
+ });
1105
+ process.stdout.on("resize", () => {
1106
+ render(state);
1107
+ });
1108
+ }
1109
+
1110
+ // src/logs-viewer.ts
1111
+ var import_fs_extra5 = __toESM(require("fs-extra"));
802
1112
  var import_node_path6 = __toESM(require("path"));
803
1113
  function isRunMetadata(value) {
804
1114
  if (!value || typeof value !== "object") return false;
@@ -811,10 +1121,10 @@ function buildLogMetaPath(logsDir, logFile) {
811
1121
  }
812
1122
  async function readLogMetadata(logsDir, logFile) {
813
1123
  const metaPath = buildLogMetaPath(logsDir, logFile);
814
- const exists = await import_fs_extra4.default.pathExists(metaPath);
1124
+ const exists = await import_fs_extra5.default.pathExists(metaPath);
815
1125
  if (!exists) return void 0;
816
1126
  try {
817
- const content = await import_fs_extra4.default.readFile(metaPath, "utf8");
1127
+ const content = await import_fs_extra5.default.readFile(metaPath, "utf8");
818
1128
  const parsed = JSON.parse(content);
819
1129
  return isRunMetadata(parsed) ? parsed : void 0;
820
1130
  } catch {
@@ -832,10 +1142,10 @@ function buildRunningLogKeys(registry) {
832
1142
  return keys;
833
1143
  }
834
1144
  async function loadLogEntries(logsDir, registry) {
835
- const exists = await import_fs_extra4.default.pathExists(logsDir);
1145
+ const exists = await import_fs_extra5.default.pathExists(logsDir);
836
1146
  if (!exists) return [];
837
1147
  const running = buildRunningLogKeys(registry);
838
- const names = await import_fs_extra4.default.readdir(logsDir);
1148
+ const names = await import_fs_extra5.default.readdir(logsDir);
839
1149
  const entries = [];
840
1150
  for (const name of names) {
841
1151
  if (import_node_path6.default.extname(name).toLowerCase() !== ".log") continue;
@@ -843,7 +1153,7 @@ async function loadLogEntries(logsDir, registry) {
843
1153
  const filePath = import_node_path6.default.join(logsDir, name);
844
1154
  let stat;
845
1155
  try {
846
- stat = await import_fs_extra4.default.stat(filePath);
1156
+ stat = await import_fs_extra5.default.stat(filePath);
847
1157
  } catch {
848
1158
  continue;
849
1159
  }
@@ -859,12 +1169,12 @@ async function loadLogEntries(logsDir, registry) {
859
1169
  }
860
1170
  return entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
861
1171
  }
862
- function getTerminalSize() {
1172
+ function getTerminalSize2() {
863
1173
  const rows = process.stdout.rows ?? 24;
864
1174
  const columns = process.stdout.columns ?? 80;
865
1175
  return { rows, columns };
866
1176
  }
867
- function truncateLine(line, width) {
1177
+ function truncateLine2(line, width) {
868
1178
  if (width <= 0) return "";
869
1179
  if (line.length <= width) return line;
870
1180
  return line.slice(0, width);
@@ -886,12 +1196,12 @@ function formatBytes(size) {
886
1196
  const mb = kb / 1024;
887
1197
  return `${mb.toFixed(1)}MB`;
888
1198
  }
889
- function getPageSize(rows) {
1199
+ function getPageSize2(rows) {
890
1200
  return Math.max(1, rows - 2);
891
1201
  }
892
1202
  async function readLogLines(logFile) {
893
1203
  try {
894
- const content = await import_fs_extra4.default.readFile(logFile, "utf8");
1204
+ const content = await import_fs_extra5.default.readFile(logFile, "utf8");
895
1205
  const normalized = content.replace(/\r\n?/g, "\n");
896
1206
  const lines = normalized.split("\n");
897
1207
  return lines.length > 0 ? lines : [""];
@@ -903,36 +1213,36 @@ async function readLogLines(logFile) {
903
1213
  function buildListHeader(state, columns) {
904
1214
  const total = state.logs.length;
905
1215
  const title = `\u65E5\u5FD7\u5217\u8868\uFF08${total} \u6761\uFF09\uFF5C\u2191/\u2193 \u9009\u62E9 Enter \u67E5\u770B q \u9000\u51FA`;
906
- return truncateLine(title, columns);
1216
+ return truncateLine2(title, columns);
907
1217
  }
908
1218
  function buildListStatus(state, columns) {
909
1219
  if (state.logs.length === 0) {
910
1220
  const text = state.lastError ? `\u52A0\u8F7D\u5931\u8D25\uFF1A${state.lastError}` : "\u6682\u65E0\u53EF\u67E5\u770B\u7684\u65E5\u5FD7";
911
- return truncateLine(text, columns);
1221
+ return truncateLine2(text, columns);
912
1222
  }
913
1223
  const entry = state.logs[state.selectedIndex];
914
1224
  const meta = entry.meta;
915
1225
  const detail = meta ? `\u9879\u76EE ${meta.path}` : `\u6587\u4EF6 ${entry.fileName}`;
916
1226
  const suffix = state.lastError ? ` \uFF5C \u52A0\u8F7D\u5931\u8D25\uFF1A${state.lastError}` : "";
917
- return truncateLine(`${detail}${suffix}`, columns);
1227
+ return truncateLine2(`${detail}${suffix}`, columns);
918
1228
  }
919
- function buildListLine(entry, selected, columns) {
1229
+ function buildListLine2(entry, selected, columns) {
920
1230
  const marker = selected ? ">" : " ";
921
1231
  const time = formatTimestamp(entry.mtimeMs);
922
1232
  const metaInfo = entry.meta ? `\u8F6E\u6B21 ${entry.meta.round} \uFF5C Token ${entry.meta.tokenUsed}` : `\u5927\u5C0F ${formatBytes(entry.size)}`;
923
- return truncateLine(`${marker} ${entry.fileName} \uFF5C ${time} \uFF5C ${metaInfo}`, columns);
1233
+ return truncateLine2(`${marker} ${entry.fileName} \uFF5C ${time} \uFF5C ${metaInfo}`, columns);
924
1234
  }
925
1235
  function buildViewHeader(entry, columns) {
926
- const title = `\u65E5\u5FD7\u67E5\u770B\uFF5C${entry.fileName}\uFF5C\u2191/\u2193 \u7FFB\u9875 b \u8FD4\u56DE q \u9000\u51FA`;
927
- return truncateLine(title, columns);
1236
+ const title = `\u65E5\u5FD7\u67E5\u770B\uFF5C${entry.fileName}\uFF5C\u2191/\u2193 \u4E0A\u4E0B 1 \u884C PageUp/PageDown \u7FFB\u9875 b \u8FD4\u56DE q \u9000\u51FA`;
1237
+ return truncateLine2(title, columns);
928
1238
  }
929
1239
  function buildViewStatus(entry, page, columns) {
930
1240
  const meta = entry.meta;
931
1241
  const metaInfo = meta ? `\u8F6E\u6B21 ${meta.round} \uFF5C Token ${meta.tokenUsed} \uFF5C \u9879\u76EE ${meta.path}` : `\u6587\u4EF6 ${entry.fileName}`;
932
1242
  const status = `\u9875 ${page.current}/${page.total} \uFF5C ${metaInfo}`;
933
- return truncateLine(status, columns);
1243
+ return truncateLine2(status, columns);
934
1244
  }
935
- function ensureListOffset(state, pageSize) {
1245
+ function ensureListOffset2(state, pageSize) {
936
1246
  const total = state.logs.length;
937
1247
  if (total === 0) {
938
1248
  state.listOffset = 0;
@@ -949,10 +1259,10 @@ function ensureListOffset(state, pageSize) {
949
1259
  state.listOffset = Math.min(Math.max(state.listOffset, 0), maxOffset);
950
1260
  }
951
1261
  function renderList(state) {
952
- const { rows, columns } = getTerminalSize();
953
- const pageSize = getPageSize(rows);
1262
+ const { rows, columns } = getTerminalSize2();
1263
+ const pageSize = getPageSize2(rows);
954
1264
  const header = buildListHeader(state, columns);
955
- ensureListOffset(state, pageSize);
1265
+ ensureListOffset2(state, pageSize);
956
1266
  if (state.logs.length === 0) {
957
1267
  const filler = Array.from({ length: pageSize }, () => "");
958
1268
  const status2 = buildListStatus(state, columns);
@@ -964,7 +1274,7 @@ function renderList(state) {
964
1274
  const slice = state.logs.slice(start, start + pageSize);
965
1275
  const lines = slice.map((entry, index) => {
966
1276
  const selected = start + index === state.selectedIndex;
967
- return buildListLine(entry, selected, columns);
1277
+ return buildListLine2(entry, selected, columns);
968
1278
  });
969
1279
  while (lines.length < pageSize) {
970
1280
  lines.push("");
@@ -974,28 +1284,30 @@ function renderList(state) {
974
1284
  process.stdout.write(`\x1B[2J\x1B[H${content}`);
975
1285
  }
976
1286
  function renderView(view) {
977
- const { rows, columns } = getTerminalSize();
978
- const pageSize = getPageSize(rows);
1287
+ const { rows, columns } = getTerminalSize2();
1288
+ const pageSize = getPageSize2(rows);
979
1289
  const header = buildViewHeader(view.entry, columns);
980
- const maxOffset = Math.max(0, Math.ceil(view.lines.length / pageSize) - 1);
981
- view.pageOffset = Math.min(Math.max(view.pageOffset, 0), maxOffset);
982
- const start = view.pageOffset * pageSize;
983
- const pageLines = view.lines.slice(start, start + pageSize).map((line) => truncateLine(line, columns));
1290
+ const maxOffset = Math.max(0, view.lines.length - pageSize);
1291
+ view.lineOffset = Math.min(Math.max(view.lineOffset, 0), maxOffset);
1292
+ const start = view.lineOffset;
1293
+ const pageLines = view.lines.slice(start, start + pageSize).map((line) => truncateLine2(line, columns));
984
1294
  while (pageLines.length < pageSize) {
985
1295
  pageLines.push("");
986
1296
  }
987
- const status = buildViewStatus(view.entry, { current: view.pageOffset + 1, total: Math.max(1, maxOffset + 1) }, columns);
1297
+ const totalPages = Math.max(1, Math.ceil(view.lines.length / pageSize));
1298
+ const currentPage = Math.min(totalPages, Math.floor(view.lineOffset / pageSize) + 1);
1299
+ const status = buildViewStatus(view.entry, { current: currentPage, total: totalPages }, columns);
988
1300
  const content = [header, ...pageLines, status].join("\n");
989
1301
  process.stdout.write(`\x1B[2J\x1B[H${content}`);
990
1302
  }
991
- function render(state) {
1303
+ function render2(state) {
992
1304
  if (state.mode === "view" && state.view) {
993
1305
  renderView(state.view);
994
1306
  return;
995
1307
  }
996
1308
  renderList(state);
997
1309
  }
998
- function shouldExit(input) {
1310
+ function shouldExit2(input) {
999
1311
  if (input === "") return true;
1000
1312
  if (input.toLowerCase() === "q") return true;
1001
1313
  return false;
@@ -1003,16 +1315,22 @@ function shouldExit(input) {
1003
1315
  function isEnter(input) {
1004
1316
  return input.includes("\r") || input.includes("\n");
1005
1317
  }
1006
- function isArrowUp(input) {
1318
+ function isArrowUp2(input) {
1007
1319
  return input.includes("\x1B[A");
1008
1320
  }
1009
- function isArrowDown(input) {
1321
+ function isArrowDown2(input) {
1010
1322
  return input.includes("\x1B[B");
1011
1323
  }
1324
+ function isPageUp(input) {
1325
+ return input.includes("\x1B[5~");
1326
+ }
1327
+ function isPageDown(input) {
1328
+ return input.includes("\x1B[6~");
1329
+ }
1012
1330
  function isEscape(input) {
1013
1331
  return input === "\x1B";
1014
1332
  }
1015
- function setupCleanup(cleanup) {
1333
+ function setupCleanup2(cleanup) {
1016
1334
  const exitHandler = () => {
1017
1335
  cleanup();
1018
1336
  };
@@ -1024,7 +1342,7 @@ function setupCleanup(cleanup) {
1024
1342
  process.on("SIGTERM", signalHandler);
1025
1343
  process.on("exit", exitHandler);
1026
1344
  }
1027
- function clampIndex(value, total) {
1345
+ function clampIndex2(value, total) {
1028
1346
  if (total <= 0) return 0;
1029
1347
  return Math.min(Math.max(value, 0), total - 1);
1030
1348
  }
@@ -1050,7 +1368,7 @@ async function runLogsViewer() {
1050
1368
  }
1051
1369
  process.stdout.write("\x1B[?25h");
1052
1370
  };
1053
- setupCleanup(cleanup);
1371
+ setupCleanup2(cleanup);
1054
1372
  process.stdout.write("\x1B[?25l");
1055
1373
  process.stdin.setRawMode(true);
1056
1374
  process.stdin.resume();
@@ -1059,7 +1377,7 @@ async function runLogsViewer() {
1059
1377
  try {
1060
1378
  const registry = await readCurrentRegistry();
1061
1379
  state.logs = await loadLogEntries(logsDir, registry);
1062
- state.selectedIndex = clampIndex(state.selectedIndex, state.logs.length);
1380
+ state.selectedIndex = clampIndex2(state.selectedIndex, state.logs.length);
1063
1381
  state.lastError = void 0;
1064
1382
  } catch (error) {
1065
1383
  const message = error instanceof Error ? error.message : String(error);
@@ -1076,37 +1394,37 @@ async function runLogsViewer() {
1076
1394
  state.view = {
1077
1395
  entry,
1078
1396
  lines: ["\u52A0\u8F7D\u4E2D\u2026"],
1079
- pageOffset: 0
1397
+ lineOffset: 0
1080
1398
  };
1081
- render(state);
1399
+ render2(state);
1082
1400
  const lines = await readLogLines(entry.filePath);
1083
- const pageSize = getPageSize(getTerminalSize().rows);
1084
- const maxOffset = Math.max(0, Math.ceil(lines.length / pageSize) - 1);
1401
+ const pageSize = getPageSize2(getTerminalSize2().rows);
1402
+ const maxOffset = Math.max(0, lines.length - pageSize);
1085
1403
  state.view = {
1086
1404
  entry,
1087
1405
  lines,
1088
- pageOffset: maxOffset
1406
+ lineOffset: maxOffset
1089
1407
  };
1090
1408
  loading = false;
1091
- render(state);
1409
+ render2(state);
1092
1410
  };
1093
1411
  await loadLogs();
1094
- render(state);
1412
+ render2(state);
1095
1413
  process.stdin.on("data", (data) => {
1096
1414
  const input = data.toString("utf8");
1097
- if (shouldExit(input)) {
1415
+ if (shouldExit2(input)) {
1098
1416
  cleanup();
1099
1417
  process.exit(0);
1100
1418
  }
1101
1419
  if (state.mode === "list") {
1102
- if (isArrowUp(input)) {
1103
- state.selectedIndex = clampIndex(state.selectedIndex - 1, state.logs.length);
1104
- render(state);
1420
+ if (isArrowUp2(input)) {
1421
+ state.selectedIndex = clampIndex2(state.selectedIndex - 1, state.logs.length);
1422
+ render2(state);
1105
1423
  return;
1106
1424
  }
1107
- if (isArrowDown(input)) {
1108
- state.selectedIndex = clampIndex(state.selectedIndex + 1, state.logs.length);
1109
- render(state);
1425
+ if (isArrowDown2(input)) {
1426
+ state.selectedIndex = clampIndex2(state.selectedIndex + 1, state.logs.length);
1427
+ render2(state);
1110
1428
  return;
1111
1429
  }
1112
1430
  if (isEnter(input)) {
@@ -1116,63 +1434,267 @@ async function runLogsViewer() {
1116
1434
  return;
1117
1435
  }
1118
1436
  if (state.mode === "view" && state.view) {
1119
- if (isArrowUp(input)) {
1120
- state.view.pageOffset -= 1;
1121
- render(state);
1437
+ if (isArrowUp2(input)) {
1438
+ state.view.lineOffset -= 1;
1439
+ render2(state);
1440
+ return;
1441
+ }
1442
+ if (isArrowDown2(input)) {
1443
+ state.view.lineOffset += 1;
1444
+ render2(state);
1122
1445
  return;
1123
1446
  }
1124
- if (isArrowDown(input)) {
1125
- state.view.pageOffset += 1;
1126
- render(state);
1447
+ if (isPageUp(input)) {
1448
+ const pageSize = getPageSize2(getTerminalSize2().rows);
1449
+ state.view.lineOffset -= pageSize;
1450
+ render2(state);
1451
+ return;
1452
+ }
1453
+ if (isPageDown(input)) {
1454
+ const pageSize = getPageSize2(getTerminalSize2().rows);
1455
+ state.view.lineOffset += pageSize;
1456
+ render2(state);
1127
1457
  return;
1128
1458
  }
1129
1459
  if (input.toLowerCase() === "b" || isEscape(input)) {
1130
1460
  state.mode = "list";
1131
1461
  state.view = void 0;
1132
- render(state);
1462
+ render2(state);
1133
1463
  return;
1134
1464
  }
1135
1465
  }
1136
1466
  });
1137
1467
  process.stdout.on("resize", () => {
1138
- render(state);
1468
+ render2(state);
1139
1469
  });
1140
1470
  }
1141
1471
 
1142
1472
  // src/loop.ts
1143
- var import_fs_extra7 = __toESM(require("fs-extra"));
1144
- var import_node_path8 = __toESM(require("path"));
1473
+ var import_fs_extra9 = __toESM(require("fs-extra"));
1474
+ var import_node_path9 = __toESM(require("path"));
1145
1475
 
1146
1476
  // src/ai.ts
1147
- function buildPrompt(input) {
1148
- const sections = [
1477
+ function compactLine(text) {
1478
+ return text.replace(/\s+/g, " ").trim();
1479
+ }
1480
+ function buildBranchNamePrompt(input) {
1481
+ return [
1482
+ "# \u89D2\u8272",
1483
+ "\u4F60\u662F\u8D44\u6DF1\u5DE5\u7A0B\u5E08\uFF0C\u9700\u8981\u6839\u636E\u4EFB\u52A1\u751F\u6210\u89C4\u8303\u7684 git \u5206\u652F\u540D\u3002",
1484
+ "# \u89C4\u5219",
1485
+ "- \u8F93\u51FA\u683C\u5F0F\u4EC5\u9650\u4E25\u683C JSON\uFF08\u4E0D\u8981 markdown\u3001\u4E0D\u8981\u4EE3\u7801\u5757\u3001\u4E0D\u8981\u89E3\u91CA\uFF09\u3002",
1486
+ "- \u5206\u652F\u540D\u683C\u5F0F\uFF1A<type>/<slug>\u3002",
1487
+ "- type \u53EF\u9009\uFF1Afeat\u3001fix\u3001docs\u3001refactor\u3001chore\u3001test\u3002",
1488
+ "- slug \u4F7F\u7528\u5C0F\u5199\u82F1\u6587\u3001\u6570\u5B57\u3001\u8FDE\u5B57\u7B26\uFF0C\u957F\u5EA6 3~40\uFF0C\u907F\u514D\u7A7A\u683C\u4E0E\u4E2D\u6587\u3002",
1489
+ "# \u8F93\u51FA JSON",
1490
+ '{"branch":"..."}',
1491
+ "# \u4EFB\u52A1\u63CF\u8FF0",
1492
+ compactLine(input.task) || "\uFF08\u7A7A\uFF09"
1493
+ ].join("\n\n");
1494
+ }
1495
+ function buildPlanningPrompt(input) {
1496
+ return [
1149
1497
  "# \u80CC\u666F\u4EFB\u52A1",
1150
1498
  input.task,
1499
+ "# \u5206\u652F\u4FE1\u606F",
1500
+ input.branchName ? `\u8BA1\u5212\u4F7F\u7528\u5206\u652F\uFF1A${input.branchName}` : "\u672A\u6307\u5B9A\u5206\u652F\u540D\uFF0C\u8BF7\u6309\u4EFB\u52A1\u8BED\u4E49\u7ED9\u51FA\u5EFA\u8BAE",
1151
1501
  "# \u5DE5\u4F5C\u6D41\u7A0B\u57FA\u7EBF\uFF08\u4F9B AI \u81EA\u4E3B\u6267\u884C\uFF09",
1152
1502
  input.workflowGuide,
1153
- "# \u5F53\u524D\u6301\u4E45\u5316\u8BA1\u5212",
1154
- input.plan || "\uFF08\u6682\u65E0\u8BA1\u5212\uFF0C\u9996\u8F6E\u8BF7\u751F\u6210\u53EF\u6267\u884C\u8BA1\u5212\u5E76\u5199\u5165 plan \u6587\u4EF6\uFF09",
1155
- "# \u5386\u53F2\u8FED\u4EE3\u4E0E\u8BB0\u5FC6",
1156
- input.notes || "\uFF08\u9996\u6B21\u6267\u884C\uFF0C\u6682\u65E0\u5386\u53F2\uFF09",
1503
+ "# \u5F53\u524D\u8BA1\u5212",
1504
+ input.plan || "\uFF08\u6682\u65E0\u8BA1\u5212\uFF09",
1505
+ "# \u5386\u53F2\u8BB0\u5FC6",
1506
+ input.notes || "\uFF08\u6682\u65E0\u5386\u53F2\uFF09",
1157
1507
  "# \u672C\u8F6E\u6267\u884C\u8981\u6C42",
1158
1508
  [
1159
- "1. \u81EA\u6211\u68C0\u67E5\u5E76\u8865\u5168\u9700\u6C42\uFF1B\u660E\u786E\u4EA4\u4ED8\u7269\u4E0E\u9A8C\u6536\u6807\u51C6\u3002",
1160
- "2. \u66F4\u65B0/\u7EC6\u5316\u8BA1\u5212\uFF0C\u5FC5\u8981\u65F6\u5728 plan \u6587\u4EF6\u4E2D\u91CD\u5199\u4EFB\u52A1\u6811\u4E0E\u4F18\u5148\u7EA7\u3002",
1161
- "3. \u8BBE\u8BA1\u5F00\u53D1\u6B65\u9AA4\u5E76\u76F4\u63A5\u751F\u6210\u4EE3\u7801\uFF08\u65E0\u9700\u518D\u6B21\u8BF7\u6C42\u786E\u8BA4\uFF09\u3002",
1162
- "4. \u8FDB\u884C\u4EE3\u7801\u81EA\u5BA1\uFF0C\u7ED9\u51FA\u98CE\u9669\u4E0E\u6539\u8FDB\u6E05\u5355\u3002",
1163
- "5. \u751F\u6210\u5355\u5143\u6D4B\u8BD5\u4E0E e2e \u6D4B\u8BD5\u4EE3\u7801\u5E76\u7ED9\u51FA\u8FD0\u884C\u547D\u4EE4\uFF1B\u5982\u679C\u73AF\u5883\u5141\u8BB8\u53EF\u76F4\u63A5\u8FD0\u884C\u547D\u4EE4\u3002",
1164
- "6. \u7EF4\u62A4\u6301\u4E45\u5316\u8BB0\u5FC6\u6587\u4EF6\uFF1A\u6458\u8981\u672C\u8F6E\u5173\u952E\u7ED3\u8BBA\u3001\u9057\u7559\u95EE\u9898\u3001\u4E0B\u4E00\u6B65\u5EFA\u8BAE\u3002",
1165
- "7. \u51C6\u5907\u63D0\u4EA4 PR \u6240\u9700\u7684\u6807\u9898\u4E0E\u63CF\u8FF0\uFF08\u542B\u53D8\u66F4\u6458\u8981\u3001\u6D4B\u8BD5\u7ED3\u679C\u3001\u98CE\u9669\uFF09\u3002",
1166
- "8. \u5F53\u6240\u6709\u76EE\u6807\u5B8C\u6210\u65F6\uFF0C\u5728\u8F93\u51FA\u4E2D\u52A0\u5165\u6807\u8BB0 <<DONE>> \u4EE5\u4FBF\u5916\u5C42\u505C\u6B62\u5FAA\u73AF\u3002"
1509
+ "1. \u5206\u6790\u4EFB\u52A1\u8F93\u5165/\u8F93\u51FA/\u7EA6\u675F/\u9A8C\u6536\u6807\u51C6\uFF0C\u5FC5\u8981\u65F6\u8865\u5145\u5408\u7406\u5047\u8BBE\uFF08\u5199\u5165 notes\uFF09\u3002",
1510
+ "2. \u82E5 plan.md \u5DF2\u5B58\u5728\uFF0C\u8BF7\u5224\u65AD\u662F\u5426\u5408\u7406\uFF1B\u5408\u7406\u5219\u4E0D\u4FEE\u6539\uFF0C\u4E0D\u5408\u7406\u5219\u4F18\u5316\u6216\u91CD\u5199\u3002",
1511
+ "3. \u8BA1\u5212\u53EA\u5305\u542B\u5F00\u53D1\u76F8\u5173\u4EFB\u52A1\uFF08\u8BBE\u8BA1/\u5B9E\u73B0/\u91CD\u6784/\u914D\u7F6E/\u6587\u6863\u66F4\u65B0\uFF09\uFF0C\u4E0D\u8981\u5305\u542B\u6D4B\u8BD5\u3001\u81EA\u5BA1\u3001PR\u3001\u63D0\u4EA4\u7B49\u5185\u5BB9\u3002",
1512
+ "4. \u8BA1\u5212\u9879\u9700\u53EF\u6267\u884C\u3001\u9897\u7C92\u5EA6\u6E05\u6670\uFF0C\u5DF2\u5B8C\u6210\u9879\u4F7F\u7528 \u2705 \u6807\u8BB0\u3002",
1513
+ "5. \u66F4\u65B0 memory/plan.md \u4E0E memory/notes.md \u540E\u7ED3\u675F\u672C\u8F6E\u3002"
1167
1514
  ].join("\n")
1168
- ];
1169
- return sections.join("\n\n");
1515
+ ].join("\n\n");
1170
1516
  }
1171
- function pickNumber(pattern, text) {
1172
- const match = pattern.exec(text);
1173
- if (!match || match.length < 2) return void 0;
1174
- const value = Number.parseInt(match[match.length - 1], 10);
1175
- return Number.isNaN(value) ? void 0 : value;
1517
+ function buildPlanItemPrompt(input) {
1518
+ return [
1519
+ "# \u80CC\u666F\u4EFB\u52A1",
1520
+ input.task,
1521
+ "# \u5DE5\u4F5C\u6D41\u7A0B\u57FA\u7EBF\uFF08\u4F9B AI \u81EA\u4E3B\u6267\u884C\uFF09",
1522
+ input.workflowGuide,
1523
+ "# \u5F53\u524D\u8BA1\u5212",
1524
+ input.plan || "\uFF08\u6682\u65E0\u8BA1\u5212\uFF09",
1525
+ "# \u5386\u53F2\u8BB0\u5FC6",
1526
+ input.notes || "\uFF08\u6682\u65E0\u5386\u53F2\uFF09",
1527
+ "# \u672C\u8F6E\u8981\u6267\u884C\u7684\u8BA1\u5212\u9879\uFF08\u4EC5\u6B64\u4E00\u6761\uFF09",
1528
+ input.item,
1529
+ "# \u672C\u8F6E\u6267\u884C\u8981\u6C42",
1530
+ [
1531
+ "1. \u53EA\u6267\u884C\u4E0A\u8FF0\u8BA1\u5212\u9879\uFF0C\u907F\u514D\u63D0\u524D\u5904\u7406\u5176\u5B83\u8BA1\u5212\u9879\u3002",
1532
+ "2. \u5B8C\u6210\u540E\u7ACB\u5373\u5728 plan.md \u4E2D\u5C06\u8BE5\u9879\u6807\u8BB0\u4E3A \u2705\u3002",
1533
+ "3. \u5FC5\u8981\u65F6\u53EF\u5BF9\u8BA1\u5212\u9879\u8FDB\u884C\u5FAE\u8C03\uFF0C\u4F46\u4ECD\u9700\u786E\u4FDD\u5F53\u524D\u9879\u5B8C\u6210\u3002",
1534
+ "4. \u672C\u8F6E\u4E0D\u6267\u884C\u6D4B\u8BD5\u6216\u8D28\u91CF\u68C0\u67E5\u3002",
1535
+ "5. \u5C06\u8FDB\u5C55\u3001\u5173\u952E\u6539\u52A8\u4E0E\u98CE\u9669\u5199\u5165 notes\u3002"
1536
+ ].join("\n")
1537
+ ].join("\n\n");
1538
+ }
1539
+ function buildQualityPrompt(input) {
1540
+ return [
1541
+ "# \u80CC\u666F\u4EFB\u52A1",
1542
+ input.task,
1543
+ "# \u5DE5\u4F5C\u6D41\u7A0B\u57FA\u7EBF\uFF08\u4F9B AI \u81EA\u4E3B\u6267\u884C\uFF09",
1544
+ input.workflowGuide,
1545
+ "# \u5F53\u524D\u8BA1\u5212",
1546
+ input.plan || "\uFF08\u6682\u65E0\u8BA1\u5212\uFF09",
1547
+ "# \u5386\u53F2\u8BB0\u5FC6",
1548
+ input.notes || "\uFF08\u6682\u65E0\u5386\u53F2\uFF09",
1549
+ "# \u672C\u8F6E\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5",
1550
+ input.commands.length > 0 ? input.commands.map((cmd) => `- ${cmd}`).join("\n") : "\u672A\u68C0\u6D4B\u5230\u53EF\u6267\u884C\u7684\u8D28\u91CF\u68C0\u67E5\u547D\u4EE4\u3002",
1551
+ input.results ? `# \u547D\u4EE4\u6267\u884C\u7ED3\u679C
1552
+ ${input.results}` : "",
1553
+ "# \u672C\u8F6E\u6267\u884C\u8981\u6C42",
1554
+ [
1555
+ "1. \u672C\u8F6E\u4EC5\u8FDB\u884C\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5\uFF0C\u4E0D\u8981\u4FEE\u590D\u95EE\u9898\u3002",
1556
+ "2. \u82E5\u51FA\u73B0\u5931\u8D25\uFF0C\u8BB0\u5F55\u5931\u8D25\u8981\u70B9\uFF0C\u7B49\u5F85\u4E0B\u4E00\u8F6E\u4FEE\u590D\u3002",
1557
+ "3. \u5C06\u7ED3\u8BBA\u4E0E\u98CE\u9669\u5199\u5165 notes\u3002"
1558
+ ].join("\n")
1559
+ ].filter(Boolean).join("\n\n");
1560
+ }
1561
+ function buildFixPrompt(input) {
1562
+ return [
1563
+ "# \u80CC\u666F\u4EFB\u52A1",
1564
+ input.task,
1565
+ "# \u5DE5\u4F5C\u6D41\u7A0B\u57FA\u7EBF\uFF08\u4F9B AI \u81EA\u4E3B\u6267\u884C\uFF09",
1566
+ input.workflowGuide,
1567
+ "# \u5F53\u524D\u8BA1\u5212",
1568
+ input.plan || "\uFF08\u6682\u65E0\u8BA1\u5212\uFF09",
1569
+ "# \u5386\u53F2\u8BB0\u5FC6",
1570
+ input.notes || "\uFF08\u6682\u65E0\u5386\u53F2\uFF09",
1571
+ `# \u9700\u8981\u4FEE\u590D\u7684\u95EE\u9898\uFF08${input.stage}\uFF09`,
1572
+ input.errors || "\uFF08\u65E0\u9519\u8BEF\u4FE1\u606F\uFF09",
1573
+ "# \u672C\u8F6E\u6267\u884C\u8981\u6C42",
1574
+ [
1575
+ "1. \u805A\u7126\u4FEE\u590D\u5F53\u524D\u95EE\u9898\uFF0C\u4E0D\u8981\u6269\u5C55\u8303\u56F4\u3002",
1576
+ "2. \u4FEE\u590D\u5B8C\u6210\u540E\u66F4\u65B0 notes\uFF0C\u8BF4\u660E\u4FEE\u6539\u70B9\u4E0E\u5F71\u54CD\u3002",
1577
+ "3. \u5982\u9700\u8C03\u6574\u8BA1\u5212\uFF0C\u8BF7\u540C\u6B65\u66F4\u65B0 plan.md\u3002"
1578
+ ].join("\n")
1579
+ ].join("\n\n");
1580
+ }
1581
+ function buildTestPrompt(input) {
1582
+ return [
1583
+ "# \u80CC\u666F\u4EFB\u52A1",
1584
+ input.task,
1585
+ "# \u5DE5\u4F5C\u6D41\u7A0B\u57FA\u7EBF\uFF08\u4F9B AI \u81EA\u4E3B\u6267\u884C\uFF09",
1586
+ input.workflowGuide,
1587
+ "# \u5F53\u524D\u8BA1\u5212",
1588
+ input.plan || "\uFF08\u6682\u65E0\u8BA1\u5212\uFF09",
1589
+ "# \u5386\u53F2\u8BB0\u5FC6",
1590
+ input.notes || "\uFF08\u6682\u65E0\u5386\u53F2\uFF09",
1591
+ "# \u672C\u8F6E\u6D4B\u8BD5\u547D\u4EE4",
1592
+ input.commands.length > 0 ? input.commands.map((cmd) => `- ${cmd}`).join("\n") : "\u672A\u914D\u7F6E\u6D4B\u8BD5\u547D\u4EE4\u3002",
1593
+ input.results ? `# \u6D4B\u8BD5\u7ED3\u679C
1594
+ ${input.results}` : "",
1595
+ "# \u672C\u8F6E\u6267\u884C\u8981\u6C42",
1596
+ [
1597
+ "1. \u672C\u8F6E\u4EC5\u6267\u884C\u6D4B\u8BD5\uFF0C\u4E0D\u8981\u4FEE\u590D\u95EE\u9898\u3002",
1598
+ "2. \u82E5\u51FA\u73B0\u5931\u8D25\uFF0C\u8BB0\u5F55\u5931\u8D25\u8981\u70B9\uFF0C\u7B49\u5F85\u4E0B\u4E00\u8F6E\u4FEE\u590D\u3002",
1599
+ "3. \u5C06\u6D4B\u8BD5\u7ED3\u8BBA\u5199\u5165 notes\u3002"
1600
+ ].join("\n")
1601
+ ].filter(Boolean).join("\n\n");
1602
+ }
1603
+ function buildDocsPrompt(input) {
1604
+ return [
1605
+ "# \u80CC\u666F\u4EFB\u52A1",
1606
+ input.task,
1607
+ "# \u5DE5\u4F5C\u6D41\u7A0B\u57FA\u7EBF\uFF08\u4F9B AI \u81EA\u4E3B\u6267\u884C\uFF09",
1608
+ input.workflowGuide,
1609
+ "# \u5F53\u524D\u8BA1\u5212",
1610
+ input.plan || "\uFF08\u6682\u65E0\u8BA1\u5212\uFF09",
1611
+ "# \u5386\u53F2\u8BB0\u5FC6",
1612
+ input.notes || "\uFF08\u6682\u65E0\u5386\u53F2\uFF09",
1613
+ "# \u672C\u8F6E\u6267\u884C\u8981\u6C42",
1614
+ [
1615
+ "1. \u6839\u636E\u672C\u6B21\u6539\u52A8\u66F4\u65B0\u7248\u672C\u53F7\u3001CHANGELOG\u3001README\u3001docs \u7B49\u76F8\u5173\u6587\u6863\u3002",
1616
+ "2. \u4EC5\u66F4\u65B0\u786E\u6709\u53D8\u5316\u7684\u6587\u6863\uFF0C\u4FDD\u6301\u4E2D\u6587\u8BF4\u660E\u3002",
1617
+ "3. \u5C06\u66F4\u65B0\u6458\u8981\u5199\u5165 notes\u3002"
1618
+ ].join("\n")
1619
+ ].join("\n\n");
1620
+ }
1621
+ function extractJson(text) {
1622
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
1623
+ if (fenced?.[1]) return fenced[1].trim();
1624
+ const start = text.indexOf("{");
1625
+ const end = text.lastIndexOf("}");
1626
+ if (start >= 0 && end > start) {
1627
+ return text.slice(start, end + 1).trim();
1628
+ }
1629
+ return null;
1630
+ }
1631
+ var BRANCH_TYPES = ["feat", "fix", "docs", "refactor", "chore", "test"];
1632
+ var BRANCH_TYPE_ALIASES = {
1633
+ feature: "feat",
1634
+ features: "feat",
1635
+ bugfix: "fix",
1636
+ hotfix: "fix",
1637
+ doc: "docs",
1638
+ documentation: "docs",
1639
+ refactoring: "refactor",
1640
+ chores: "chore",
1641
+ tests: "test"
1642
+ };
1643
+ function isBranchType(value) {
1644
+ return BRANCH_TYPES.includes(value);
1645
+ }
1646
+ function normalizeBranchType(value) {
1647
+ const trimmed = value.trim().toLowerCase();
1648
+ if (!trimmed) return null;
1649
+ if (isBranchType(trimmed)) return trimmed;
1650
+ return BRANCH_TYPE_ALIASES[trimmed] ?? null;
1651
+ }
1652
+ function normalizeBranchSlug(value) {
1653
+ const cleaned = value.toLowerCase().replace(/\s+/g, "-").replace(/_/g, "-").replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
1654
+ if (!cleaned) return null;
1655
+ const trimmed = cleaned.slice(0, 40);
1656
+ if (trimmed.length < 3) return null;
1657
+ return trimmed;
1658
+ }
1659
+ function normalizeBranchNameCandidate(value) {
1660
+ const trimmed = value.trim();
1661
+ if (!trimmed) return null;
1662
+ const lowered = trimmed.toLowerCase();
1663
+ const parts = lowered.split("/").filter((part) => part.length > 0);
1664
+ const hasExplicitType = lowered.includes("/") && parts.length >= 2;
1665
+ const rawType = hasExplicitType ? parts.shift() ?? "" : "";
1666
+ const rawSlug = hasExplicitType ? parts.join("-") : lowered;
1667
+ const type = rawType ? normalizeBranchType(rawType) : "feat";
1668
+ if (!type) return null;
1669
+ const slug = normalizeBranchSlug(rawSlug);
1670
+ if (!slug) return null;
1671
+ return `${type}/${slug}`;
1672
+ }
1673
+ function parseBranchName(output) {
1674
+ const jsonText = extractJson(output);
1675
+ if (jsonText) {
1676
+ try {
1677
+ const parsed = JSON.parse(jsonText);
1678
+ const raw = typeof parsed.branch === "string" ? parsed.branch : typeof parsed.branchName === "string" ? parsed.branchName : typeof parsed["\u5206\u652F"] === "string" ? parsed["\u5206\u652F"] : typeof parsed["\u5206\u652F\u540D"] === "string" ? parsed["\u5206\u652F\u540D"] : null;
1679
+ if (raw) {
1680
+ const normalized = normalizeBranchNameCandidate(raw);
1681
+ if (normalized) return normalized;
1682
+ }
1683
+ } catch {
1684
+ }
1685
+ }
1686
+ const lineMatch = output.match(/(?:branch(?:name)?|分支名|分支)\s*[::]\s*([^\s]+)/i);
1687
+ if (lineMatch?.[1]) {
1688
+ const normalized = normalizeBranchNameCandidate(lineMatch[1]);
1689
+ if (normalized) return normalized;
1690
+ }
1691
+ return null;
1692
+ }
1693
+ function pickNumber(pattern, text) {
1694
+ const match = pattern.exec(text);
1695
+ if (!match || match.length < 2) return void 0;
1696
+ const value = Number.parseInt(match[match.length - 1], 10);
1697
+ return Number.isNaN(value) ? void 0 : value;
1176
1698
  }
1177
1699
  function parseTokenUsage(logs) {
1178
1700
  const total = pickNumber(/total[_\s]tokens:\s*(\d+)/i, logs);
@@ -1244,8 +1766,9 @@ async function runAi(prompt, ai, logger, cwd) {
1244
1766
  };
1245
1767
  }
1246
1768
  function formatIterationRecord(record) {
1769
+ const title = record.stage ? `### \u8FED\u4EE3 ${record.iteration} \uFF5C ${record.timestamp} \uFF5C ${record.stage}` : `### \u8FED\u4EE3 ${record.iteration} \uFF5C ${record.timestamp}`;
1247
1770
  const lines = [
1248
- `### \u8FED\u4EE3 ${record.iteration} \uFF5C ${record.timestamp}`,
1771
+ title,
1249
1772
  "",
1250
1773
  "#### \u63D0\u793A\u4E0A\u4E0B\u6587",
1251
1774
  "```",
@@ -1258,6 +1781,19 @@ function formatIterationRecord(record) {
1258
1781
  "```",
1259
1782
  ""
1260
1783
  ];
1784
+ if (record.checkResults && record.checkResults.length > 0) {
1785
+ lines.push("#### \u8D28\u91CF\u68C0\u67E5\u7ED3\u679C");
1786
+ record.checkResults.forEach((result) => {
1787
+ const status = result.success ? "\u2705 \u901A\u8FC7" : "\u274C \u5931\u8D25";
1788
+ lines.push(`${status} \uFF5C ${result.name} \uFF5C \u547D\u4EE4: ${result.command} \uFF5C \u9000\u51FA\u7801: ${result.exitCode}`);
1789
+ if (!result.success) {
1790
+ lines.push("```");
1791
+ lines.push(result.stderr || result.stdout || "\uFF08\u65E0\u8F93\u51FA\uFF09");
1792
+ lines.push("```");
1793
+ lines.push("");
1794
+ }
1795
+ });
1796
+ }
1261
1797
  if (record.testResults && record.testResults.length > 0) {
1262
1798
  lines.push("#### \u6D4B\u8BD5\u7ED3\u679C");
1263
1799
  record.testResults.forEach((result) => {
@@ -1277,7 +1813,7 @@ function formatIterationRecord(record) {
1277
1813
 
1278
1814
  // src/deps.ts
1279
1815
  var import_node_path7 = __toESM(require("path"));
1280
- var import_fs_extra5 = __toESM(require("fs-extra"));
1816
+ var import_fs_extra6 = __toESM(require("fs-extra"));
1281
1817
  function parsePackageManagerField(value) {
1282
1818
  if (!value) return null;
1283
1819
  const normalized = value.trim().toLowerCase();
@@ -1375,20 +1911,20 @@ function extractPackageManagerField(value) {
1375
1911
  }
1376
1912
  async function readPackageManagerHints(cwd, logger) {
1377
1913
  const packageJsonPath = import_node_path7.default.join(cwd, "package.json");
1378
- const hasPackageJson = await import_fs_extra5.default.pathExists(packageJsonPath);
1914
+ const hasPackageJson = await import_fs_extra6.default.pathExists(packageJsonPath);
1379
1915
  if (!hasPackageJson) return null;
1380
1916
  let packageManagerField;
1381
1917
  try {
1382
- const packageJson = await import_fs_extra5.default.readJson(packageJsonPath);
1918
+ const packageJson = await import_fs_extra6.default.readJson(packageJsonPath);
1383
1919
  packageManagerField = extractPackageManagerField(packageJson);
1384
1920
  } catch (error) {
1385
1921
  logger.warn(`\u8BFB\u53D6 package.json \u5931\u8D25\uFF0C\u5C06\u6539\u7528\u9501\u6587\u4EF6\u5224\u65AD\u5305\u7BA1\u7406\u5668: ${String(error)}`);
1386
1922
  }
1387
1923
  const [hasYarnLock, hasPnpmLock, hasNpmLock, hasNpmShrinkwrap] = await Promise.all([
1388
- import_fs_extra5.default.pathExists(import_node_path7.default.join(cwd, "yarn.lock")),
1389
- import_fs_extra5.default.pathExists(import_node_path7.default.join(cwd, "pnpm-lock.yaml")),
1390
- import_fs_extra5.default.pathExists(import_node_path7.default.join(cwd, "package-lock.json")),
1391
- import_fs_extra5.default.pathExists(import_node_path7.default.join(cwd, "npm-shrinkwrap.json"))
1924
+ import_fs_extra6.default.pathExists(import_node_path7.default.join(cwd, "yarn.lock")),
1925
+ import_fs_extra6.default.pathExists(import_node_path7.default.join(cwd, "pnpm-lock.yaml")),
1926
+ import_fs_extra6.default.pathExists(import_node_path7.default.join(cwd, "package-lock.json")),
1927
+ import_fs_extra6.default.pathExists(import_node_path7.default.join(cwd, "npm-shrinkwrap.json"))
1392
1928
  ]);
1393
1929
  return {
1394
1930
  packageManagerField,
@@ -1592,9 +2128,25 @@ async function listFailedRuns(branch, cwd, logger) {
1592
2128
  return [];
1593
2129
  }
1594
2130
  }
2131
+ async function enableAutoMerge(target, cwd, logger) {
2132
+ const targetValue = String(target);
2133
+ const args = ["pr", "merge", targetValue, "--auto", "--merge"];
2134
+ const result = await runCommand("gh", args, {
2135
+ cwd,
2136
+ logger,
2137
+ verboseLabel: "gh",
2138
+ verboseCommand: `gh ${args.join(" ")}`
2139
+ });
2140
+ if (result.exitCode !== 0) {
2141
+ logger.warn(`\u542F\u7528\u81EA\u52A8\u5408\u5E76\u5931\u8D25: ${result.stderr || result.stdout}`);
2142
+ return false;
2143
+ }
2144
+ logger.success("\u5DF2\u542F\u7528 PR \u81EA\u52A8\u5408\u5E76");
2145
+ return true;
2146
+ }
1595
2147
 
1596
2148
  // src/logger.ts
1597
- var import_fs_extra6 = __toESM(require("fs-extra"));
2149
+ var import_fs_extra7 = __toESM(require("fs-extra"));
1598
2150
  var wrap = (code) => (value) => `\x1B[${code}m${value}\x1B[0m`;
1599
2151
  var colors = {
1600
2152
  blue: wrap("34"),
@@ -1613,7 +2165,7 @@ var Logger = class {
1613
2165
  this.logFileErrored = false;
1614
2166
  if (this.logFile) {
1615
2167
  try {
1616
- import_fs_extra6.default.ensureFileSync(this.logFile);
2168
+ import_fs_extra7.default.ensureFileSync(this.logFile);
1617
2169
  } catch (error) {
1618
2170
  this.disableFileWithError(error);
1619
2171
  }
@@ -1653,7 +2205,7 @@ var Logger = class {
1653
2205
  writeFileLine(line) {
1654
2206
  if (!this.logFileEnabled || !this.logFile) return;
1655
2207
  try {
1656
- import_fs_extra6.default.appendFileSync(this.logFile, `${line}
2208
+ import_fs_extra7.default.appendFileSync(this.logFile, `${line}
1657
2209
  `, "utf8");
1658
2210
  } catch (error) {
1659
2211
  this.disableFileWithError(error);
@@ -1679,6 +2231,70 @@ var Logger = class {
1679
2231
  };
1680
2232
  var defaultLogger = new Logger();
1681
2233
 
2234
+ // src/plan.ts
2235
+ var ITEM_PATTERN = /^(\s*)([-*+]|\d+\.)\s+(.*)$/;
2236
+ function isCompleted(content) {
2237
+ if (content.includes("\u2705")) return true;
2238
+ if (/\[[xX]\]/.test(content)) return true;
2239
+ return false;
2240
+ }
2241
+ function normalizeText(content) {
2242
+ return content.replace(/\[[xX ]\]\s*/g, "").replace(/✅/g, "").trim();
2243
+ }
2244
+ function parsePlanItems(plan) {
2245
+ const lines = plan.split(/\r?\n/);
2246
+ const items = [];
2247
+ lines.forEach((line, index) => {
2248
+ const match = line.match(ITEM_PATTERN);
2249
+ if (!match) return;
2250
+ const content = match[3] ?? "";
2251
+ const text = normalizeText(content);
2252
+ if (!text) return;
2253
+ items.push({
2254
+ index,
2255
+ raw: line,
2256
+ text,
2257
+ completed: isCompleted(content)
2258
+ });
2259
+ });
2260
+ return items;
2261
+ }
2262
+ function getPendingPlanItems(plan) {
2263
+ return parsePlanItems(plan).filter((item) => !item.completed);
2264
+ }
2265
+
2266
+ // src/quality.ts
2267
+ var import_fs_extra8 = __toESM(require("fs-extra"));
2268
+ var import_node_path8 = __toESM(require("path"));
2269
+ function hasScript(scripts, name) {
2270
+ return typeof scripts[name] === "string" && scripts[name].trim().length > 0;
2271
+ }
2272
+ async function detectQualityCommands(workDir) {
2273
+ const packagePath = import_node_path8.default.join(workDir, "package.json");
2274
+ const exists = await import_fs_extra8.default.pathExists(packagePath);
2275
+ if (!exists) return [];
2276
+ const pkg = await import_fs_extra8.default.readJson(packagePath);
2277
+ const scripts = typeof pkg === "object" && pkg && typeof pkg.scripts === "object" ? pkg.scripts ?? {} : {};
2278
+ const commands = [];
2279
+ const seen = /* @__PURE__ */ new Set();
2280
+ const append = (name, command) => {
2281
+ if (seen.has(name)) return;
2282
+ if (!hasScript(scripts, name)) return;
2283
+ commands.push({ name, command });
2284
+ seen.add(name);
2285
+ };
2286
+ append("lint", "yarn lint");
2287
+ append("lint:ci", "yarn lint:ci");
2288
+ append("lint:check", "yarn lint:check");
2289
+ append("typecheck", "yarn typecheck");
2290
+ append("format:check", "yarn format:check");
2291
+ append("format:ci", "yarn format:ci");
2292
+ if (!hasScript(scripts, "format:check") && !hasScript(scripts, "format:ci")) {
2293
+ append("format", "yarn format");
2294
+ }
2295
+ return commands;
2296
+ }
2297
+
1682
2298
  // src/runtime-tracker.ts
1683
2299
  async function safeWrite(logFile, metadata, logger) {
1684
2300
  try {
@@ -1703,14 +2319,15 @@ async function safeRemove(logFile, logger) {
1703
2319
  }
1704
2320
  }
1705
2321
  async function createRunTracker(options) {
1706
- const { logFile, command, path: path10, logger } = options;
2322
+ const { logFile, command, path: path12, logger } = options;
1707
2323
  if (!logFile) return null;
1708
2324
  const update = async (round, tokenUsed) => {
1709
2325
  const metadata = {
1710
2326
  command,
1711
2327
  round,
1712
2328
  tokenUsed,
1713
- path: path10
2329
+ path: path12,
2330
+ pid: process.pid
1714
2331
  };
1715
2332
  await safeWrite(logFile, metadata, logger);
1716
2333
  };
@@ -1725,14 +2342,14 @@ async function createRunTracker(options) {
1725
2342
 
1726
2343
  // src/summary.ts
1727
2344
  var REQUIRED_SECTIONS = ["# \u53D8\u66F4\u6458\u8981", "# \u6D4B\u8BD5\u7ED3\u679C", "# \u98CE\u9669\u4E0E\u56DE\u6EDA"];
1728
- function normalizeText(text) {
2345
+ function normalizeText2(text) {
1729
2346
  return text.replace(/\r\n?/g, "\n");
1730
2347
  }
1731
- function compactLine(text) {
2348
+ function compactLine2(text) {
1732
2349
  return text.replace(/\s+/g, " ").trim();
1733
2350
  }
1734
2351
  function trimTail(text, limit, emptyFallback) {
1735
- const normalized = normalizeText(text).trim();
2352
+ const normalized = normalizeText2(text).trim();
1736
2353
  if (!normalized) return emptyFallback;
1737
2354
  if (normalized.length <= limit) return normalized;
1738
2355
  return `\uFF08\u5185\u5BB9\u8FC7\u957F\uFF0C\u4FDD\u7559\u6700\u540E ${limit} \u5B57\u7B26\uFF09
@@ -1759,7 +2376,7 @@ function buildSummaryLinesFromCommit(commitTitle, commitBody) {
1759
2376
  return [`- ${summary}`];
1760
2377
  }
1761
2378
  function stripCommitType(title) {
1762
- const trimmed = compactLine(title);
2379
+ const trimmed = compactLine2(title);
1763
2380
  if (!trimmed) return "\u66F4\u65B0\u8FED\u4EE3\u4EA7\u51FA";
1764
2381
  const match = trimmed.match(/^[^:]+:\s*(.+)$/);
1765
2382
  return match?.[1]?.trim() || trimmed;
@@ -1798,7 +2415,7 @@ function buildSummaryPrompt(input) {
1798
2415
  "# \u8F93\u51FA JSON",
1799
2416
  '{"commitTitle":"...","commitBody":"...","prTitle":"...","prBody":"..."}',
1800
2417
  "# \u8F93\u5165\u4FE1\u606F",
1801
- `\u4EFB\u52A1: ${compactLine(input.task) || "\uFF08\u7A7A\uFF09"}`,
2418
+ `\u4EFB\u52A1: ${compactLine2(input.task) || "\uFF08\u7A7A\uFF09"}`,
1802
2419
  `\u5206\u652F: ${input.branchName ?? "\uFF08\u672A\u77E5\uFF09"}`,
1803
2420
  "\u8BA1\u5212\uFF08\u8282\u9009\uFF09:",
1804
2421
  planSnippet,
@@ -1823,7 +2440,7 @@ function pickString(record, keys) {
1823
2440
  }
1824
2441
  return null;
1825
2442
  }
1826
- function extractJson(text) {
2443
+ function extractJson2(text) {
1827
2444
  const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
1828
2445
  if (fenced?.[1]) return fenced[1].trim();
1829
2446
  const start = text.indexOf("{");
@@ -1834,21 +2451,21 @@ function extractJson(text) {
1834
2451
  return null;
1835
2452
  }
1836
2453
  function normalizeTitle(title) {
1837
- return compactLine(title);
2454
+ return compactLine2(title);
1838
2455
  }
1839
2456
  function normalizeBody(body) {
1840
2457
  if (!body) return void 0;
1841
- const normalized = normalizeText(body).trim();
2458
+ const normalized = normalizeText2(body).trim();
1842
2459
  return normalized.length > 0 ? normalized : void 0;
1843
2460
  }
1844
2461
  function extractBulletLines(text) {
1845
2462
  if (!text) return [];
1846
- const lines = normalizeText(text).split("\n").map((line) => line.trim()).filter(Boolean);
2463
+ const lines = normalizeText2(text).split("\n").map((line) => line.trim()).filter(Boolean);
1847
2464
  const bullets = lines.filter((line) => line.startsWith("- ") || line.startsWith("* "));
1848
2465
  return bullets.map((line) => line.startsWith("* ") ? `- ${line.slice(2).trim()}` : line);
1849
2466
  }
1850
2467
  function parseDeliverySummary(output) {
1851
- const jsonText = extractJson(output);
2468
+ const jsonText = extractJson2(output);
1852
2469
  if (!jsonText) return null;
1853
2470
  try {
1854
2471
  const parsed = JSON.parse(jsonText);
@@ -1872,7 +2489,7 @@ function parseDeliverySummary(output) {
1872
2489
  const normalizedCommitTitle = normalizeTitle(commitTitle);
1873
2490
  const normalizedPrTitle = normalizeTitle(prTitle);
1874
2491
  const normalizedCommitBody = normalizeBody(commitBody);
1875
- const normalizedPrBody = normalizeText(prBody).trim();
2492
+ const normalizedPrBody = normalizeText2(prBody).trim();
1876
2493
  if (!normalizedCommitTitle || !normalizedPrTitle || !normalizedPrBody) return null;
1877
2494
  return {
1878
2495
  commitTitle: normalizedCommitTitle,
@@ -1885,7 +2502,7 @@ function parseDeliverySummary(output) {
1885
2502
  }
1886
2503
  }
1887
2504
  function buildFallbackSummary(input) {
1888
- const taskLine = compactLine(input.task);
2505
+ const taskLine = compactLine2(input.task);
1889
2506
  const shortTask = taskLine.length > 50 ? `${taskLine.slice(0, 50)}...` : taskLine;
1890
2507
  const baseTitle = shortTask || "\u66F4\u65B0\u8FED\u4EE3\u4EA7\u51FA";
1891
2508
  const title = `chore: ${baseTitle}`;
@@ -1900,7 +2517,7 @@ function buildFallbackSummary(input) {
1900
2517
  };
1901
2518
  }
1902
2519
  function ensurePrBodySections(prBody, fallback) {
1903
- const normalized = normalizeText(prBody).trim();
2520
+ const normalized = normalizeText2(prBody).trim();
1904
2521
  const hasAll = REQUIRED_SECTIONS.every((section) => normalized.includes(section));
1905
2522
  if (hasAll) return normalized;
1906
2523
  const summaryLines = buildSummaryLinesFromCommit(fallback.commitTitle, fallback.commitBody);
@@ -1972,13 +2589,18 @@ async function ensureWorkflowFiles(workflowFiles) {
1972
2589
  await ensureFile(workflowFiles.planFile, "# \u8BA1\u5212\n");
1973
2590
  await ensureFile(workflowFiles.notesFile, "# \u6301\u4E45\u5316\u8BB0\u5FC6\n");
1974
2591
  }
1975
- var MAX_TEST_LOG_LENGTH = 4e3;
1976
- function trimOutput(output, limit = MAX_TEST_LOG_LENGTH) {
2592
+ var MAX_LOG_LENGTH = 4e3;
2593
+ function trimOutput(output, limit = MAX_LOG_LENGTH) {
1977
2594
  if (!output) return "";
1978
2595
  if (output.length <= limit) return output;
1979
2596
  return `${output.slice(0, limit)}
1980
2597
  \u2026\u2026\uFF08\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u539F\u59CB\u957F\u5EA6 ${output.length} \u5B57\u7B26\uFF09`;
1981
2598
  }
2599
+ function truncateText(text, limit = 24) {
2600
+ const trimmed = text.trim();
2601
+ if (trimmed.length <= limit) return trimmed;
2602
+ return `${trimmed.slice(0, limit)}...`;
2603
+ }
1982
2604
  async function safeCommandOutput(command, args, cwd, logger, label, verboseCommand) {
1983
2605
  const result = await runCommand(command, args, {
1984
2606
  cwd,
@@ -2016,6 +2638,66 @@ async function runSingleTest(kind, command, cwd, logger) {
2016
2638
  stderr: trimOutput(result.stderr.trim())
2017
2639
  };
2018
2640
  }
2641
+ async function runQualityChecks(commands, cwd, logger) {
2642
+ const results = [];
2643
+ for (const item of commands) {
2644
+ logger.info(`\u6267\u884C\u8D28\u91CF\u68C0\u67E5: ${item.command}`);
2645
+ const result = await runCommand("bash", ["-lc", item.command], {
2646
+ cwd,
2647
+ logger,
2648
+ verboseLabel: "shell",
2649
+ verboseCommand: `bash -lc "${item.command}"`
2650
+ });
2651
+ results.push({
2652
+ name: item.name,
2653
+ command: item.command,
2654
+ success: result.exitCode === 0,
2655
+ exitCode: result.exitCode,
2656
+ stdout: trimOutput(result.stdout.trim()),
2657
+ stderr: trimOutput(result.stderr.trim())
2658
+ });
2659
+ }
2660
+ return results;
2661
+ }
2662
+ function buildCheckResultSummary(results) {
2663
+ if (results.length === 0) return "\uFF08\u672A\u6267\u884C\u8D28\u91CF\u68C0\u67E5\uFF09";
2664
+ return results.map((result) => {
2665
+ const status = result.success ? "\u901A\u8FC7" : `\u5931\u8D25\uFF08\u9000\u51FA\u7801 ${result.exitCode}\uFF09`;
2666
+ const output = result.success ? "" : `
2667
+ ${result.stderr || result.stdout || "\uFF08\u65E0\u8F93\u51FA\uFF09"}`;
2668
+ return `- ${result.name}: ${status}\uFF5C\u547D\u4EE4: ${result.command}${output}`;
2669
+ }).join("\n");
2670
+ }
2671
+ function buildFailedCheckSummary(results) {
2672
+ return buildCheckResultSummary(results.filter((result) => !result.success));
2673
+ }
2674
+ function buildTestResultSummary(results) {
2675
+ if (results.length === 0) return "\uFF08\u672A\u6267\u884C\u6D4B\u8BD5\uFF09";
2676
+ return results.map((result) => {
2677
+ const label = result.kind === "unit" ? "\u5355\u5143\u6D4B\u8BD5" : "e2e \u6D4B\u8BD5";
2678
+ const status = result.success ? "\u901A\u8FC7" : `\u5931\u8D25\uFF08\u9000\u51FA\u7801 ${result.exitCode}\uFF09`;
2679
+ const output = result.success ? "" : `
2680
+ ${result.stderr || result.stdout || "\uFF08\u65E0\u8F93\u51FA\uFF09"}`;
2681
+ return `- ${label}: ${status}\uFF5C\u547D\u4EE4: ${result.command}${output}`;
2682
+ }).join("\n");
2683
+ }
2684
+ function buildFailedTestSummary(results) {
2685
+ return buildTestResultSummary(results.filter((result) => !result.success));
2686
+ }
2687
+ function formatSystemRecord(stage, detail, timestamp) {
2688
+ return [
2689
+ `### \u8BB0\u5F55 \uFF5C ${timestamp} \uFF5C ${stage}`,
2690
+ "",
2691
+ detail,
2692
+ ""
2693
+ ].join("\n");
2694
+ }
2695
+ function shouldSkipQuality(content, cliSkip) {
2696
+ if (cliSkip) return true;
2697
+ const normalized = content.replace(/\s+/g, "");
2698
+ if (!normalized) return false;
2699
+ return normalized.includes("\u4E0D\u8981\u68C0\u67E5\u4EE3\u7801\u8D28\u91CF") || normalized.includes("\u4E0D\u68C0\u67E5\u4EE3\u7801\u8D28\u91CF") || normalized.includes("\u8DF3\u8FC7\u4EE3\u7801\u8D28\u91CF");
2700
+ }
2019
2701
  async function runTests(config, workDir, logger) {
2020
2702
  const results = [];
2021
2703
  if (config.runTests && config.tests.unitCommand) {
@@ -2028,10 +2710,26 @@ async function runTests(config, workDir, logger) {
2028
2710
  }
2029
2711
  return results;
2030
2712
  }
2713
+ async function runTestsSafely(config, workDir, logger) {
2714
+ try {
2715
+ return await runTests(config, workDir, logger);
2716
+ } catch (error) {
2717
+ const errorMessage = String(error);
2718
+ logger.warn(`\u6D4B\u8BD5\u6267\u884C\u5F02\u5E38: ${errorMessage}`);
2719
+ return [{
2720
+ kind: "unit",
2721
+ command: config.tests.unitCommand ?? "\u672A\u77E5\u6D4B\u8BD5\u547D\u4EE4",
2722
+ success: false,
2723
+ exitCode: -1,
2724
+ stdout: "",
2725
+ stderr: trimOutput(errorMessage)
2726
+ }];
2727
+ }
2728
+ }
2031
2729
  function reRootPath(filePath, repoRoot, workDir) {
2032
- const relative = import_node_path8.default.relative(repoRoot, filePath);
2730
+ const relative = import_node_path9.default.relative(repoRoot, filePath);
2033
2731
  if (relative.startsWith("..")) return filePath;
2034
- return import_node_path8.default.join(workDir, relative);
2732
+ return import_node_path9.default.join(workDir, relative);
2035
2733
  }
2036
2734
  function reRootWorkflowFiles(workflowFiles, repoRoot, workDir) {
2037
2735
  if (repoRoot === workDir) return workflowFiles;
@@ -2042,10 +2740,10 @@ function reRootWorkflowFiles(workflowFiles, repoRoot, workDir) {
2042
2740
  };
2043
2741
  }
2044
2742
  function buildBodyFile(workDir) {
2045
- return import_node_path8.default.join(workDir, "memory", "pr-body.md");
2743
+ return import_node_path9.default.join(workDir, "memory", "pr-body.md");
2046
2744
  }
2047
2745
  async function writePrBody(bodyPath, content, appendExisting) {
2048
- await import_fs_extra7.default.mkdirp(import_node_path8.default.dirname(bodyPath));
2746
+ await import_fs_extra9.default.mkdirp(import_node_path9.default.dirname(bodyPath));
2049
2747
  let finalContent = content.trim();
2050
2748
  if (appendExisting) {
2051
2749
  const existing = await readFileSafe(bodyPath);
@@ -2058,7 +2756,7 @@ async function writePrBody(bodyPath, content, appendExisting) {
2058
2756
  ${finalContent}`;
2059
2757
  }
2060
2758
  }
2061
- await import_fs_extra7.default.writeFile(bodyPath, `${finalContent}
2759
+ await import_fs_extra9.default.writeFile(bodyPath, `${finalContent}
2062
2760
  `, "utf8");
2063
2761
  }
2064
2762
  async function cleanupWorktreeIfSafe(context) {
@@ -2095,20 +2793,20 @@ async function runLoop(config) {
2095
2793
  const logger = new Logger({ verbose: config.verbose, logFile: config.logFile });
2096
2794
  const repoRoot = await getRepoRoot(config.cwd, logger);
2097
2795
  logger.debug(`\u4ED3\u5E93\u6839\u76EE\u5F55: ${repoRoot}`);
2098
- const worktreeResult = config.git.useWorktree ? await ensureWorktree(config.git, repoRoot, logger) : { path: repoRoot, created: false };
2099
- const workDir = worktreeResult.path;
2100
- const worktreeCreated = worktreeResult.created;
2101
- logger.debug(`\u5DE5\u4F5C\u76EE\u5F55: ${workDir}`);
2102
- const commandLine = formatCommandLine(process.argv);
2103
- const runTracker = await createRunTracker({
2104
- logFile: config.logFile,
2105
- command: commandLine,
2106
- path: workDir,
2107
- logger
2108
- });
2109
2796
  let branchName = config.git.branchName;
2797
+ let workDir = repoRoot;
2798
+ let worktreeCreated = false;
2799
+ const commandLine = formatCommandLine(process.argv);
2800
+ let runTracker = null;
2801
+ let accumulatedUsage = null;
2802
+ let lastTestResults = null;
2803
+ let lastAiOutput = "";
2110
2804
  let lastRound = 0;
2111
2805
  let runError = null;
2806
+ let prInfo = null;
2807
+ let prFailed = false;
2808
+ let sessionIndex = 0;
2809
+ const preWorktreeRecords = [];
2112
2810
  const notifyWebhook = async (event, iteration, stage) => {
2113
2811
  const payload = buildWebhookPayload({
2114
2812
  event,
@@ -2120,15 +2818,46 @@ async function runLoop(config) {
2120
2818
  await sendWebhookNotifications(config.webhooks, payload, logger);
2121
2819
  };
2122
2820
  try {
2123
- if (!branchName) {
2124
- try {
2125
- branchName = await getCurrentBranch(workDir, logger);
2126
- } catch (error) {
2127
- const message = error instanceof Error ? error.message : String(error);
2128
- logger.warn(`\u8BFB\u53D6\u5206\u652F\u540D\u5931\u8D25\uFF0Cwebhook \u4E2D\u5C06\u7F3A\u5931\u5206\u652F\u4FE1\u606F\uFF1A${message}`);
2821
+ await notifyWebhook("task_start", 0, "\u4EFB\u52A1\u5F00\u59CB");
2822
+ if (config.git.useWorktree && !branchName) {
2823
+ const branchPrompt = buildBranchNamePrompt({ task: config.task });
2824
+ await notifyWebhook("iteration_start", sessionIndex + 1, "\u5206\u652F\u540D\u751F\u6210");
2825
+ logger.info("\u5206\u652F\u540D\u751F\u6210\u63D0\u793A\u6784\u5EFA\u5B8C\u6210\uFF0C\u8C03\u7528 AI CLI...");
2826
+ const aiResult = await runAi(branchPrompt, config.ai, logger, repoRoot);
2827
+ accumulatedUsage = mergeTokenUsage(accumulatedUsage, aiResult.usage);
2828
+ lastAiOutput = aiResult.output;
2829
+ sessionIndex += 1;
2830
+ lastRound = sessionIndex;
2831
+ const record = formatIterationRecord({
2832
+ iteration: sessionIndex,
2833
+ stage: "\u5206\u652F\u540D\u751F\u6210",
2834
+ prompt: branchPrompt,
2835
+ aiOutput: aiResult.output,
2836
+ timestamp: isoNow()
2837
+ });
2838
+ preWorktreeRecords.push(record);
2839
+ const parsed = parseBranchName(aiResult.output);
2840
+ if (parsed) {
2841
+ branchName = parsed;
2842
+ logger.info(`AI \u751F\u6210\u5206\u652F\u540D\uFF1A${branchName}`);
2843
+ } else {
2844
+ branchName = generateBranchNameFromTask(config.task);
2845
+ logger.warn(`\u672A\u89E3\u6790\u5230 AI \u5206\u652F\u540D\uFF0C\u4F7F\u7528\u515C\u5E95\u5206\u652F\uFF1A${branchName}`);
2129
2846
  }
2130
2847
  }
2131
- await notifyWebhook("task_start", 0, "\u4EFB\u52A1\u5F00\u59CB");
2848
+ const worktreeResult = config.git.useWorktree ? await ensureWorktree({ ...config.git, branchName }, repoRoot, logger) : { path: repoRoot, created: false };
2849
+ workDir = worktreeResult.path;
2850
+ worktreeCreated = worktreeResult.created;
2851
+ logger.debug(`\u5DE5\u4F5C\u76EE\u5F55: ${workDir}`);
2852
+ runTracker = await createRunTracker({
2853
+ logFile: config.logFile,
2854
+ command: commandLine,
2855
+ path: workDir,
2856
+ logger
2857
+ });
2858
+ if (runTracker && sessionIndex > 0) {
2859
+ await runTracker.update(sessionIndex, accumulatedUsage?.totalTokens ?? 0);
2860
+ }
2132
2861
  if (config.skipInstall) {
2133
2862
  logger.info("\u5DF2\u8DF3\u8FC7\u4F9D\u8D56\u68C0\u67E5");
2134
2863
  } else {
@@ -2136,79 +2865,203 @@ async function runLoop(config) {
2136
2865
  }
2137
2866
  const workflowFiles = reRootWorkflowFiles(config.workflowFiles, repoRoot, workDir);
2138
2867
  await ensureWorkflowFiles(workflowFiles);
2868
+ if (preWorktreeRecords.length > 0) {
2869
+ for (const record of preWorktreeRecords) {
2870
+ await appendSection(workflowFiles.notesFile, record);
2871
+ }
2872
+ logger.success(`\u5DF2\u5199\u5165\u5206\u652F\u540D\u751F\u6210\u8BB0\u5F55\u81F3 ${workflowFiles.notesFile}`);
2873
+ }
2139
2874
  const planContent = await readFileSafe(workflowFiles.planFile);
2140
2875
  if (planContent.trim().length === 0) {
2141
2876
  logger.warn("plan \u6587\u4EF6\u4E3A\u7A7A\uFF0C\u5EFA\u8BAE AI \u9996\u8F6E\u751F\u6210\u8BA1\u5212");
2142
2877
  }
2878
+ if (!branchName) {
2879
+ try {
2880
+ branchName = await getCurrentBranch(workDir, logger);
2881
+ } catch (error) {
2882
+ const message = error instanceof Error ? error.message : String(error);
2883
+ logger.warn(`\u8BFB\u53D6\u5206\u652F\u540D\u5931\u8D25\uFF0Cwebhook \u4E2D\u5C06\u7F3A\u5931\u5206\u652F\u4FE1\u606F\uFF1A${message}`);
2884
+ }
2885
+ }
2143
2886
  const aiConfig = config.ai;
2144
- let accumulatedUsage = null;
2145
- let lastTestResults = null;
2146
- let lastAiOutput = "";
2147
- let prInfo = null;
2148
- let prFailed = false;
2149
- for (let i = 1; i <= config.iterations; i += 1) {
2150
- await notifyWebhook("iteration_start", i, `\u5F00\u59CB\u7B2C ${i} \u8F6E\u8FED\u4EE3`);
2151
- const workflowGuide = await readFileSafe(workflowFiles.workflowDoc);
2152
- const plan = await readFileSafe(workflowFiles.planFile);
2153
- const notes = await readFileSafe(workflowFiles.notesFile);
2154
- logger.debug(`\u52A0\u8F7D\u63D0\u793A\u4E0A\u4E0B\u6587\uFF0C\u957F\u5EA6\uFF1Aworkflow=${workflowGuide.length}, plan=${plan.length}, notes=${notes.length}`);
2155
- const prompt = buildPrompt({
2156
- task: config.task,
2157
- workflowGuide,
2158
- plan,
2159
- notes,
2160
- iteration: i
2161
- });
2162
- logger.debug(`\u7B2C ${i} \u8F6E\u63D0\u793A\u957F\u5EA6: ${prompt.length}`);
2163
- logger.info(`\u7B2C ${i} \u8F6E\u63D0\u793A\u6784\u5EFA\u5B8C\u6210\uFF0C\u8C03\u7528 AI CLI...`);
2164
- const aiResult = await runAi(prompt, aiConfig, logger, workDir);
2887
+ const loadContext = async () => ({
2888
+ workflowGuide: await readFileSafe(workflowFiles.workflowDoc),
2889
+ plan: await readFileSafe(workflowFiles.planFile),
2890
+ notes: await readFileSafe(workflowFiles.notesFile)
2891
+ });
2892
+ const runAiSession = async (stage, prompt, extras) => {
2893
+ sessionIndex += 1;
2894
+ await notifyWebhook("iteration_start", sessionIndex, stage);
2895
+ logger.info(`${stage} \u63D0\u793A\u6784\u5EFA\u5B8C\u6210\uFF0C\u8C03\u7528 AI CLI...`);
2896
+ const aiResult = await runAi(prompt, aiConfig, logger, extras?.cwd ?? workDir);
2165
2897
  accumulatedUsage = mergeTokenUsage(accumulatedUsage, aiResult.usage);
2166
2898
  lastAiOutput = aiResult.output;
2167
- const hitStop = aiResult.output.includes(config.stopSignal);
2168
- let testResults = [];
2169
- const shouldRunTests = config.runTests || config.runE2e;
2170
- if (shouldRunTests) {
2171
- try {
2172
- testResults = await runTests(config, workDir, logger);
2173
- } catch (error) {
2174
- const errorMessage = String(error);
2175
- logger.warn(`\u6D4B\u8BD5\u6267\u884C\u5F02\u5E38: ${errorMessage}`);
2176
- testResults = [{
2177
- kind: "unit",
2178
- command: config.tests.unitCommand ?? "\u672A\u77E5\u6D4B\u8BD5\u547D\u4EE4",
2179
- success: false,
2180
- exitCode: -1,
2181
- stdout: "",
2182
- stderr: trimOutput(errorMessage)
2183
- }];
2184
- }
2185
- }
2186
2899
  const record = formatIterationRecord({
2187
- iteration: i,
2900
+ iteration: sessionIndex,
2901
+ stage,
2188
2902
  prompt,
2189
2903
  aiOutput: aiResult.output,
2190
2904
  timestamp: isoNow(),
2191
- testResults
2905
+ testResults: extras?.testResults,
2906
+ checkResults: extras?.checkResults
2192
2907
  });
2193
2908
  await appendSection(workflowFiles.notesFile, record);
2194
- logger.success(`\u5DF2\u5C06\u7B2C ${i} \u8F6E\u8F93\u51FA\u5199\u5165 ${workflowFiles.notesFile}`);
2909
+ logger.success(`\u5DF2\u5C06${stage}\u8F93\u51FA\u5199\u5165 ${workflowFiles.notesFile}`);
2910
+ lastRound = sessionIndex;
2911
+ await runTracker?.update(sessionIndex, accumulatedUsage?.totalTokens ?? 0);
2912
+ };
2913
+ {
2914
+ const { workflowGuide, plan, notes } = await loadContext();
2915
+ const planningPrompt = buildPlanningPrompt({
2916
+ task: config.task,
2917
+ workflowGuide,
2918
+ plan,
2919
+ notes,
2920
+ branchName
2921
+ });
2922
+ await runAiSession("\u8BA1\u5212\u751F\u6210", planningPrompt);
2923
+ }
2924
+ let refreshedPlan = await readFileSafe(workflowFiles.planFile);
2925
+ if (/(测试|test|e2e|单测)/i.test(refreshedPlan)) {
2926
+ logger.warn("\u68C0\u6D4B\u5230 plan \u4E2D\u53EF\u80FD\u5305\u542B\u6D4B\u8BD5\u76F8\u5173\u4E8B\u9879\uFF0C\u5EFA\u8BAE\u4FDD\u7559\u5F00\u53D1\u5185\u5BB9\u5E76\u79FB\u9664\u6D4B\u8BD5\u9879\u3002");
2927
+ }
2928
+ let pendingItems = getPendingPlanItems(refreshedPlan);
2929
+ if (pendingItems.length === 0) {
2930
+ logger.info("\u8BA1\u5212\u6682\u65E0\u5F85\u6267\u884C\u9879\uFF0C\u8DF3\u8FC7\u8BA1\u5212\u6267\u884C\u5FAA\u73AF");
2931
+ const record = formatSystemRecord("\u8BA1\u5212\u6267\u884C", "\u672A\u53D1\u73B0\u5F85\u6267\u884C\u8BA1\u5212\u9879\uFF0C\u5DF2\u8DF3\u8FC7\u6267\u884C\u5FAA\u73AF\u3002", isoNow());
2932
+ await appendSection(workflowFiles.notesFile, record);
2933
+ }
2934
+ let planRounds = 0;
2935
+ while (pendingItems.length > 0) {
2936
+ if (planRounds >= config.iterations) {
2937
+ throw new Error("\u8BA1\u5212\u6267\u884C\u8FBE\u5230\u6700\u5927\u8FED\u4EE3\u6B21\u6570\uFF0C\u4ECD\u6709\u672A\u5B8C\u6210\u9879");
2938
+ }
2939
+ const lastItem = pendingItems[pendingItems.length - 1];
2940
+ const { workflowGuide, plan, notes } = await loadContext();
2941
+ const itemPrompt = buildPlanItemPrompt({
2942
+ task: config.task,
2943
+ workflowGuide,
2944
+ plan,
2945
+ notes,
2946
+ item: lastItem.text
2947
+ });
2948
+ await runAiSession(`\u6267\u884C\u8BA1\u5212\u9879\uFF1A${truncateText(lastItem.text)}`, itemPrompt);
2949
+ planRounds += 1;
2950
+ refreshedPlan = await readFileSafe(workflowFiles.planFile);
2951
+ pendingItems = getPendingPlanItems(refreshedPlan);
2952
+ }
2953
+ const agentsContent = await readFileSafe(import_node_path9.default.join(workDir, "AGENTS.md"));
2954
+ const skipQuality = shouldSkipQuality(agentsContent, config.skipQuality);
2955
+ if (skipQuality) {
2956
+ const record = formatSystemRecord("\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5", "\u5DF2\u6309\u914D\u7F6E/AGENTS.md \u8DF3\u8FC7\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5\u3002", isoNow());
2957
+ await appendSection(workflowFiles.notesFile, record);
2958
+ logger.info("\u5DF2\u8DF3\u8FC7\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5");
2959
+ } else {
2960
+ const qualityCommands = await detectQualityCommands(workDir);
2961
+ if (qualityCommands.length === 0) {
2962
+ const record = formatSystemRecord("\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5", "\u672A\u68C0\u6D4B\u5230\u53EF\u6267\u884C\u7684\u8D28\u91CF\u68C0\u67E5\u547D\u4EE4\uFF0C\u5DF2\u8DF3\u8FC7\u3002", isoNow());
2963
+ await appendSection(workflowFiles.notesFile, record);
2964
+ logger.info("\u672A\u68C0\u6D4B\u5230\u8D28\u91CF\u68C0\u67E5\u547D\u4EE4\uFF0C\u8DF3\u8FC7\u8BE5\u9636\u6BB5");
2965
+ } else {
2966
+ let qualityResults = await runQualityChecks(qualityCommands, workDir, logger);
2967
+ const { workflowGuide, plan, notes } = await loadContext();
2968
+ const qualityPrompt = buildQualityPrompt({
2969
+ task: config.task,
2970
+ workflowGuide,
2971
+ plan,
2972
+ notes,
2973
+ commands: qualityCommands.map((item) => item.command),
2974
+ results: buildCheckResultSummary(qualityResults)
2975
+ });
2976
+ await runAiSession("\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5", qualityPrompt, { checkResults: qualityResults });
2977
+ let hasQualityFailure = qualityResults.some((result) => !result.success);
2978
+ let fixRounds = 0;
2979
+ while (hasQualityFailure) {
2980
+ if (fixRounds >= config.iterations) {
2981
+ throw new Error("\u4EE3\u7801\u8D28\u91CF\u4FEE\u590D\u8FBE\u5230\u6700\u5927\u8F6E\u6B21\uFF0C\u4ECD\u672A\u901A\u8FC7");
2982
+ }
2983
+ const latest = await loadContext();
2984
+ const fixPrompt = buildFixPrompt({
2985
+ task: config.task,
2986
+ workflowGuide: latest.workflowGuide,
2987
+ plan: latest.plan,
2988
+ notes: latest.notes,
2989
+ stage: "\u4EE3\u7801\u8D28\u91CF",
2990
+ errors: buildFailedCheckSummary(qualityResults)
2991
+ });
2992
+ await runAiSession("\u4EE3\u7801\u8D28\u91CF\u4FEE\u590D", fixPrompt);
2993
+ fixRounds += 1;
2994
+ qualityResults = await runQualityChecks(qualityCommands, workDir, logger);
2995
+ hasQualityFailure = qualityResults.some((result) => !result.success);
2996
+ const recheckRecord = formatSystemRecord("\u4EE3\u7801\u8D28\u91CF\u590D\u6838", buildCheckResultSummary(qualityResults), isoNow());
2997
+ await appendSection(workflowFiles.notesFile, recheckRecord);
2998
+ }
2999
+ }
3000
+ }
3001
+ if (config.runTests || config.runE2e) {
3002
+ let testResults = await runTestsSafely(config, workDir, logger);
2195
3003
  lastTestResults = testResults;
2196
- lastRound = i;
2197
- await runTracker?.update(i, accumulatedUsage?.totalTokens ?? 0);
2198
- const hasTestFailure = testResults.some((result) => !result.success);
2199
- if (hitStop && !hasTestFailure) {
2200
- logger.info(`\u68C0\u6D4B\u5230\u505C\u6B62\u6807\u8BB0 ${config.stopSignal}\uFF0C\u63D0\u524D\u7ED3\u675F\u5FAA\u73AF`);
2201
- break;
3004
+ const testCommands = [];
3005
+ if (config.runTests && config.tests.unitCommand) {
3006
+ testCommands.push(config.tests.unitCommand);
2202
3007
  }
2203
- if (hitStop && hasTestFailure) {
2204
- logger.info(`\u68C0\u6D4B\u5230\u505C\u6B62\u6807\u8BB0 ${config.stopSignal}\uFF0C\u4F46\u6D4B\u8BD5\u5931\u8D25\uFF0C\u7EE7\u7EED\u8FDB\u5165\u4E0B\u4E00\u8F6E\u4FEE\u590D`);
3008
+ if (config.runE2e && config.tests.e2eCommand) {
3009
+ testCommands.push(config.tests.e2eCommand);
2205
3010
  }
3011
+ const { workflowGuide, plan, notes } = await loadContext();
3012
+ const testPrompt = buildTestPrompt({
3013
+ task: config.task,
3014
+ workflowGuide,
3015
+ plan,
3016
+ notes,
3017
+ commands: testCommands,
3018
+ results: buildTestResultSummary(testResults)
3019
+ });
3020
+ await runAiSession("\u6D4B\u8BD5\u6267\u884C", testPrompt, { testResults });
3021
+ let hasTestFailure = testResults.some((result) => !result.success);
3022
+ let fixRounds = 0;
3023
+ while (hasTestFailure) {
3024
+ if (fixRounds >= config.iterations) {
3025
+ throw new Error("\u6D4B\u8BD5\u4FEE\u590D\u8FBE\u5230\u6700\u5927\u8F6E\u6B21\uFF0C\u4ECD\u672A\u901A\u8FC7");
3026
+ }
3027
+ const latest = await loadContext();
3028
+ const fixPrompt = buildFixPrompt({
3029
+ task: config.task,
3030
+ workflowGuide: latest.workflowGuide,
3031
+ plan: latest.plan,
3032
+ notes: latest.notes,
3033
+ stage: "\u6D4B\u8BD5",
3034
+ errors: buildFailedTestSummary(testResults)
3035
+ });
3036
+ await runAiSession("\u6D4B\u8BD5\u4FEE\u590D", fixPrompt, { testResults });
3037
+ fixRounds += 1;
3038
+ testResults = await runTestsSafely(config, workDir, logger);
3039
+ lastTestResults = testResults;
3040
+ hasTestFailure = testResults.some((result) => !result.success);
3041
+ const recheckRecord = formatSystemRecord("\u6D4B\u8BD5\u590D\u6838", buildTestResultSummary(testResults), isoNow());
3042
+ await appendSection(workflowFiles.notesFile, recheckRecord);
3043
+ }
3044
+ } else {
3045
+ const record = formatSystemRecord("\u6D4B\u8BD5\u6267\u884C", "\u672A\u5F00\u542F\u5355\u5143\u6D4B\u8BD5\u6216 e2e \u6D4B\u8BD5\uFF0C\u5DF2\u8DF3\u8FC7\u3002", isoNow());
3046
+ await appendSection(workflowFiles.notesFile, record);
3047
+ logger.info("\u672A\u5F00\u542F\u6D4B\u8BD5\u9636\u6BB5");
3048
+ }
3049
+ {
3050
+ const { workflowGuide, plan, notes } = await loadContext();
3051
+ const docsPrompt = buildDocsPrompt({
3052
+ task: config.task,
3053
+ workflowGuide,
3054
+ plan,
3055
+ notes
3056
+ });
3057
+ await runAiSession("\u6587\u6863\u66F4\u65B0", docsPrompt);
2206
3058
  }
2207
3059
  const lastTestFailed = lastTestResults?.some((result) => !result.success) ?? false;
2208
3060
  if (lastTestFailed) {
2209
3061
  logger.warn("\u5B58\u5728\u672A\u901A\u8FC7\u7684\u6D4B\u8BD5\uFF0C\u5DF2\u8DF3\u8FC7\u81EA\u52A8\u63D0\u4EA4/\u63A8\u9001/PR");
2210
3062
  }
2211
3063
  let deliverySummary = null;
3064
+ const deliveryNotes = [];
2212
3065
  const shouldPrepareDelivery = !lastTestFailed && (config.autoCommit || config.pr.enable);
2213
3066
  if (shouldPrepareDelivery) {
2214
3067
  const [gitStatus, diffStat] = await Promise.all([
@@ -2238,53 +3091,107 @@ async function runLoop(config) {
2238
3091
  if (!deliverySummary) {
2239
3092
  deliverySummary = buildFallbackSummary({ task: config.task, testResults: lastTestResults });
2240
3093
  }
3094
+ if (deliverySummary) {
3095
+ deliveryNotes.push(`\u4EA4\u4ED8\u6458\u8981\uFF1A\u63D0\u4EA4 ${deliverySummary.commitTitle}\uFF5CPR ${deliverySummary.prTitle}`);
3096
+ }
2241
3097
  }
2242
3098
  await runTracker?.update(lastRound, accumulatedUsage?.totalTokens ?? 0);
2243
- if (config.autoCommit && !lastTestFailed) {
2244
- const summary = deliverySummary ?? buildFallbackSummary({ task: config.task, testResults: lastTestResults });
2245
- const commitMessage = {
2246
- title: summary.commitTitle,
2247
- body: summary.commitBody
2248
- };
2249
- await commitAll(commitMessage, workDir, logger).catch((error) => {
2250
- logger.warn(String(error));
2251
- });
2252
- }
2253
- if (config.autoPush && branchName && !lastTestFailed) {
2254
- await pushBranch(branchName, workDir, logger).catch((error) => {
2255
- logger.warn(String(error));
2256
- });
3099
+ if (config.autoCommit) {
3100
+ if (lastTestFailed) {
3101
+ deliveryNotes.push("\u81EA\u52A8\u63D0\u4EA4\uFF1A\u5DF2\u8DF3\u8FC7\uFF08\u6D4B\u8BD5\u672A\u901A\u8FC7\uFF09");
3102
+ } else {
3103
+ const summary = deliverySummary ?? buildFallbackSummary({ task: config.task, testResults: lastTestResults });
3104
+ const commitMessage = {
3105
+ title: summary.commitTitle,
3106
+ body: summary.commitBody
3107
+ };
3108
+ try {
3109
+ const committed = await commitAll(commitMessage, workDir, logger);
3110
+ deliveryNotes.push(committed ? `\u81EA\u52A8\u63D0\u4EA4\uFF1A\u5DF2\u63D0\u4EA4\uFF08${commitMessage.title}\uFF09` : "\u81EA\u52A8\u63D0\u4EA4\uFF1A\u672A\u751F\u6210\u63D0\u4EA4\uFF08\u53EF\u80FD\u65E0\u53D8\u66F4\u6216\u63D0\u4EA4\u5931\u8D25\uFF09");
3111
+ } catch (error) {
3112
+ deliveryNotes.push(`\u81EA\u52A8\u63D0\u4EA4\uFF1A\u5931\u8D25\uFF08${String(error)}\uFF09`);
3113
+ }
3114
+ }
3115
+ } else {
3116
+ deliveryNotes.push("\u81EA\u52A8\u63D0\u4EA4\uFF1A\u672A\u5F00\u542F");
2257
3117
  }
2258
- if (config.pr.enable && branchName && !lastTestFailed) {
2259
- logger.info("\u5F00\u59CB\u521B\u5EFA PR...");
2260
- const summary = deliverySummary ?? buildFallbackSummary({ task: config.task, testResults: lastTestResults });
2261
- const prTitleCandidate = config.pr.title?.trim() || summary.prTitle;
2262
- const prBodyContent = ensurePrBodySections(summary.prBody, {
2263
- commitTitle: summary.commitTitle,
2264
- commitBody: summary.commitBody,
2265
- testResults: lastTestResults
2266
- });
2267
- const bodyFile = config.pr.bodyPath ?? buildBodyFile(workDir);
2268
- await writePrBody(bodyFile, prBodyContent, Boolean(config.pr.bodyPath));
2269
- const createdPr = await createPr(branchName, { ...config.pr, title: prTitleCandidate, bodyPath: bodyFile }, workDir, logger);
2270
- prInfo = createdPr;
2271
- if (createdPr) {
2272
- logger.success(`PR \u5DF2\u521B\u5EFA: ${createdPr.url}`);
2273
- const failedRuns = await listFailedRuns(branchName, workDir, logger);
2274
- if (failedRuns.length > 0) {
2275
- failedRuns.forEach((run) => {
2276
- logger.warn(`Actions \u5931\u8D25: ${run.name} (${run.status}/${run.conclusion ?? "unknown"}) ${run.url}`);
2277
- });
3118
+ if (config.autoPush) {
3119
+ if (lastTestFailed) {
3120
+ deliveryNotes.push("\u81EA\u52A8\u63A8\u9001\uFF1A\u5DF2\u8DF3\u8FC7\uFF08\u6D4B\u8BD5\u672A\u901A\u8FC7\uFF09");
3121
+ } else if (!branchName) {
3122
+ deliveryNotes.push("\u81EA\u52A8\u63A8\u9001\uFF1A\u5DF2\u8DF3\u8FC7\uFF08\u7F3A\u5C11\u5206\u652F\u540D\uFF09");
3123
+ } else {
3124
+ try {
3125
+ await pushBranch(branchName, workDir, logger);
3126
+ deliveryNotes.push(`\u81EA\u52A8\u63A8\u9001\uFF1A\u5DF2\u63A8\u9001\uFF08${branchName}\uFF09`);
3127
+ } catch (error) {
3128
+ deliveryNotes.push(`\u81EA\u52A8\u63A8\u9001\uFF1A\u5931\u8D25\uFF08${String(error)}\uFF09`);
2278
3129
  }
3130
+ }
3131
+ } else {
3132
+ deliveryNotes.push("\u81EA\u52A8\u63A8\u9001\uFF1A\u672A\u5F00\u542F");
3133
+ }
3134
+ if (config.pr.enable) {
3135
+ if (lastTestFailed) {
3136
+ deliveryNotes.push("PR \u521B\u5EFA\uFF1A\u5DF2\u8DF3\u8FC7\uFF08\u6D4B\u8BD5\u672A\u901A\u8FC7\uFF09");
3137
+ } else if (!branchName) {
3138
+ deliveryNotes.push("PR \u521B\u5EFA\uFF1A\u5DF2\u8DF3\u8FC7\uFF08\u7F3A\u5C11\u5206\u652F\u540D\uFF09");
2279
3139
  } else {
2280
- prFailed = true;
2281
- logger.error("PR \u521B\u5EFA\u5931\u8D25\uFF0C\u8BE6\u89C1\u4E0A\u65B9 gh \u8F93\u51FA");
3140
+ logger.info("\u5F00\u59CB\u521B\u5EFA PR...");
3141
+ const summary = deliverySummary ?? buildFallbackSummary({ task: config.task, testResults: lastTestResults });
3142
+ const prTitleCandidate = config.pr.title?.trim() || summary.prTitle;
3143
+ const prBodyContent = ensurePrBodySections(summary.prBody, {
3144
+ commitTitle: summary.commitTitle,
3145
+ commitBody: summary.commitBody,
3146
+ testResults: lastTestResults
3147
+ });
3148
+ const bodyFile = config.pr.bodyPath ?? buildBodyFile(workDir);
3149
+ await writePrBody(bodyFile, prBodyContent, Boolean(config.pr.bodyPath));
3150
+ const createdPr = await createPr(branchName, { ...config.pr, title: prTitleCandidate, bodyPath: bodyFile }, workDir, logger);
3151
+ prInfo = createdPr;
3152
+ if (createdPr) {
3153
+ logger.success(`PR \u5DF2\u521B\u5EFA: ${createdPr.url}`);
3154
+ deliveryNotes.push(`PR \u521B\u5EFA\uFF1A\u5DF2\u5B8C\u6210\uFF08${createdPr.url}\uFF09`);
3155
+ if (config.pr.autoMerge) {
3156
+ const target = createdPr.number > 0 ? createdPr.number : createdPr.url;
3157
+ const merged = await enableAutoMerge(target, workDir, logger);
3158
+ if (merged) {
3159
+ deliveryNotes.push("PR \u81EA\u52A8\u5408\u5E76\uFF1A\u5DF2\u542F\u7528");
3160
+ } else {
3161
+ deliveryNotes.push("PR \u81EA\u52A8\u5408\u5E76\uFF1A\u542F\u7528\u5931\u8D25");
3162
+ prFailed = true;
3163
+ }
3164
+ } else {
3165
+ deliveryNotes.push("PR \u81EA\u52A8\u5408\u5E76\uFF1A\u672A\u5F00\u542F");
3166
+ }
3167
+ const failedRuns = await listFailedRuns(branchName, workDir, logger);
3168
+ if (failedRuns.length > 0) {
3169
+ failedRuns.forEach((run) => {
3170
+ logger.warn(`Actions \u5931\u8D25: ${run.name} (${run.status}/${run.conclusion ?? "unknown"}) ${run.url}`);
3171
+ });
3172
+ }
3173
+ } else {
3174
+ prFailed = true;
3175
+ deliveryNotes.push("PR \u521B\u5EFA\uFF1A\u5931\u8D25\uFF08\u8BE6\u89C1 gh \u8F93\u51FA\uFF09");
3176
+ logger.error("PR \u521B\u5EFA\u5931\u8D25\uFF0C\u8BE6\u89C1\u4E0A\u65B9 gh \u8F93\u51FA");
3177
+ }
2282
3178
  }
2283
- } else if (branchName && !config.pr.enable) {
3179
+ } else if (branchName) {
2284
3180
  logger.info("\u672A\u5F00\u542F PR \u521B\u5EFA\uFF08--pr \u672A\u4F20\uFF09\uFF0C\u5C1D\u8BD5\u67E5\u770B\u5DF2\u6709 PR");
2285
3181
  const existingPr = await viewPr(branchName, workDir, logger);
2286
3182
  prInfo = existingPr;
2287
- if (existingPr) logger.info(`\u5DF2\u6709 PR: ${existingPr.url}`);
3183
+ if (existingPr) {
3184
+ logger.info(`\u5DF2\u6709 PR: ${existingPr.url}`);
3185
+ deliveryNotes.push(`PR \u521B\u5EFA\uFF1A\u672A\u5F00\u542F\uFF08\u5DF2\u5B58\u5728 PR\uFF1A${existingPr.url}\uFF09`);
3186
+ } else {
3187
+ deliveryNotes.push("PR \u521B\u5EFA\uFF1A\u672A\u5F00\u542F\uFF08\u672A\u68C0\u6D4B\u5230\u5DF2\u6709 PR\uFF09");
3188
+ }
3189
+ } else {
3190
+ deliveryNotes.push("PR \u521B\u5EFA\uFF1A\u672A\u5F00\u542F\uFF08\u7F3A\u5C11\u5206\u652F\u540D\uFF09");
3191
+ }
3192
+ if (deliveryNotes.length > 0) {
3193
+ const record = formatSystemRecord("\u63D0\u4EA4\u4E0EPR", deliveryNotes.join("\n"), isoNow());
3194
+ await appendSection(workflowFiles.notesFile, record);
2288
3195
  }
2289
3196
  if (accumulatedUsage) {
2290
3197
  const input = accumulatedUsage.inputTokens ?? "-";
@@ -2307,6 +3214,7 @@ async function runLoop(config) {
2307
3214
  });
2308
3215
  }
2309
3216
  logger.success(`wheel-ai \u8FED\u4EE3\u6D41\u7A0B\u7ED3\u675F\uFF5CToken \u603B\u8BA1 ${accumulatedUsage?.totalTokens ?? "\u672A\u77E5"}`);
3217
+ return { branchName };
2310
3218
  } catch (error) {
2311
3219
  runError = error instanceof Error ? error.message : String(error);
2312
3220
  throw error;
@@ -2317,23 +3225,164 @@ async function runLoop(config) {
2317
3225
  }
2318
3226
  }
2319
3227
 
3228
+ // src/multi-task.ts
3229
+ var import_node_path10 = __toESM(require("path"));
3230
+ var MODE_ALIASES = {
3231
+ relay: "relay",
3232
+ serial: "serial",
3233
+ "serial-continue": "serial-continue",
3234
+ parallel: "parallel",
3235
+ \u63A5\u529B\u6A21\u5F0F: "relay",
3236
+ \u63A5\u529B: "relay",
3237
+ \u4E32\u884C\u6267\u884C: "serial",
3238
+ \u4E32\u884C: "serial",
3239
+ \u4E32\u884C\u6267\u884C\u4F46\u662F\u5931\u8D25\u4E5F\u7EE7\u7EED: "serial-continue",
3240
+ \u4E32\u884C\u7EE7\u7EED: "serial-continue",
3241
+ \u5E76\u884C\u6267\u884C: "parallel",
3242
+ \u5E76\u884C: "parallel"
3243
+ };
3244
+ function parseMultiTaskMode(raw) {
3245
+ if (!raw) return "relay";
3246
+ const trimmed = raw.trim();
3247
+ if (!trimmed) return "relay";
3248
+ const resolved = MODE_ALIASES[trimmed];
3249
+ if (!resolved) {
3250
+ throw new Error(`\u672A\u77E5 multi-task \u6A21\u5F0F: ${raw}`);
3251
+ }
3252
+ return resolved;
3253
+ }
3254
+ function normalizeTaskList(input) {
3255
+ if (Array.isArray(input)) {
3256
+ return input.map((task) => task.trim()).filter((task) => task.length > 0);
3257
+ }
3258
+ if (typeof input === "string") {
3259
+ const trimmed = input.trim();
3260
+ return trimmed.length > 0 ? [trimmed] : [];
3261
+ }
3262
+ return [];
3263
+ }
3264
+ function buildBranchNameSeries(branchInput, total) {
3265
+ if (total <= 0) return [];
3266
+ if (!branchInput) {
3267
+ return Array.from({ length: total }, () => void 0);
3268
+ }
3269
+ const baseName = branchInput;
3270
+ const names = [baseName];
3271
+ for (let i = 1; i < total; i += 1) {
3272
+ names.push(`${baseName}-${i + 1}`);
3273
+ }
3274
+ return names;
3275
+ }
3276
+ function appendPathSuffix(filePath, suffix) {
3277
+ const parsed = import_node_path10.default.parse(filePath);
3278
+ const nextName = parsed.name ? `${parsed.name}-${suffix}` : suffix;
3279
+ return import_node_path10.default.join(parsed.dir, `${nextName}${parsed.ext}`);
3280
+ }
3281
+ function deriveIndexedPath(basePath, index, total, label) {
3282
+ if (!basePath) return void 0;
3283
+ if (total <= 1 || index === 0) return basePath;
3284
+ return appendPathSuffix(basePath, `${label}-${index + 1}`);
3285
+ }
3286
+ function buildTaskPlans(input) {
3287
+ const total = input.tasks.length;
3288
+ if (total === 0) return [];
3289
+ const branchNames = input.useWorktree ? buildBranchNameSeries(input.branchInput, total) : input.tasks.map(() => input.branchInput);
3290
+ return input.tasks.map((task, index) => {
3291
+ const relayBaseBranch = input.useWorktree && input.mode === "relay" && index > 0 ? branchNames[index - 1] ?? input.baseBranch : input.baseBranch;
3292
+ return {
3293
+ task,
3294
+ index,
3295
+ branchName: branchNames[index],
3296
+ baseBranch: relayBaseBranch,
3297
+ worktreePath: deriveIndexedPath(input.worktreePath, index, total, "task"),
3298
+ logFile: deriveIndexedPath(input.logFile, index, total, "task")
3299
+ };
3300
+ });
3301
+ }
3302
+
2320
3303
  // src/monitor.ts
2321
- var import_fs_extra8 = __toESM(require("fs-extra"));
2322
- var import_node_path9 = __toESM(require("path"));
3304
+ var import_fs_extra10 = __toESM(require("fs-extra"));
3305
+ var import_node_path11 = __toESM(require("path"));
2323
3306
  var REFRESH_INTERVAL = 1e3;
2324
- function getTerminalSize2() {
3307
+ var TERMINATE_GRACE_MS = 800;
3308
+ function getTerminalSize3() {
2325
3309
  const rows = process.stdout.rows ?? 24;
2326
3310
  const columns = process.stdout.columns ?? 80;
2327
3311
  return { rows, columns };
2328
3312
  }
2329
- function truncateLine2(line, width) {
3313
+ function truncateLine3(line, width) {
2330
3314
  if (width <= 0) return "";
2331
3315
  if (line.length <= width) return line;
2332
3316
  return line.slice(0, width);
2333
3317
  }
3318
+ function padLine(text, width) {
3319
+ if (width <= 0) return "";
3320
+ const truncated = text.length > width ? text.slice(0, width) : text;
3321
+ const padding = width - truncated.length;
3322
+ const left = Math.floor(padding / 2);
3323
+ const right = padding - left;
3324
+ return `${" ".repeat(left)}${truncated}${" ".repeat(right)}`;
3325
+ }
3326
+ function buildConfirmDialogLines(taskKey, columns) {
3327
+ if (columns <= 0) return [];
3328
+ const message = `\u786E\u8BA4\u7EC8\u6B62\u4EFB\u52A1 ${taskKey}?`;
3329
+ const hint = "y \u786E\u8BA4 / n \u53D6\u6D88";
3330
+ const minWidth = Math.max(message.length, hint.length, 4);
3331
+ const innerWidth = Math.min(columns - 2, minWidth);
3332
+ if (innerWidth <= 0) {
3333
+ return [truncateLine3(message, columns), truncateLine3(hint, columns)];
3334
+ }
3335
+ const border = `+${"-".repeat(innerWidth)}+`;
3336
+ const lines = [
3337
+ border,
3338
+ `|${padLine(message, innerWidth)}|`,
3339
+ `|${padLine(hint, innerWidth)}|`,
3340
+ border
3341
+ ];
3342
+ return lines.map((line) => truncateLine3(line, columns));
3343
+ }
3344
+ function applyDialogOverlay(lines, dialogLines) {
3345
+ if (lines.length === 0 || dialogLines.length === 0) return;
3346
+ const start = Math.max(0, Math.floor((lines.length - dialogLines.length) / 2));
3347
+ for (let i = 0; i < dialogLines.length && start + i < lines.length; i += 1) {
3348
+ lines[start + i] = dialogLines[i];
3349
+ }
3350
+ }
3351
+ function resolveTerminationTarget(pid, platform = process.platform) {
3352
+ return platform === "win32" ? pid : -pid;
3353
+ }
3354
+ function isProcessAlive(pid) {
3355
+ if (!Number.isFinite(pid) || pid <= 0) return false;
3356
+ try {
3357
+ process.kill(pid, 0);
3358
+ return true;
3359
+ } catch (error) {
3360
+ const err = error;
3361
+ if (err?.code === "ESRCH") return false;
3362
+ return true;
3363
+ }
3364
+ }
3365
+ function sleep(ms) {
3366
+ return new Promise((resolve) => {
3367
+ setTimeout(resolve, ms);
3368
+ });
3369
+ }
3370
+ function setStatus(state, message, isError = false) {
3371
+ state.statusMessage = message;
3372
+ state.statusIsError = message ? isError : false;
3373
+ }
3374
+ function findTaskByKey(state, key) {
3375
+ return state.tasks.find((task) => task.key === key);
3376
+ }
3377
+ async function safeRemoveRegistry(logFile) {
3378
+ try {
3379
+ await removeCurrentRegistry(logFile);
3380
+ } catch {
3381
+ }
3382
+ }
2334
3383
  async function readLogLines2(logFile) {
2335
3384
  try {
2336
- const content = await import_fs_extra8.default.readFile(logFile, "utf8");
3385
+ const content = await import_fs_extra10.default.readFile(logFile, "utf8");
2337
3386
  const normalized = content.replace(/\r\n?/g, "\n");
2338
3387
  const lines = normalized.split("\n");
2339
3388
  return lines.length > 0 ? lines : [""];
@@ -2345,8 +3394,18 @@ async function readLogLines2(logFile) {
2345
3394
  async function loadTasks(logsDir) {
2346
3395
  const registry = await readCurrentRegistry();
2347
3396
  const entries = Object.entries(registry).sort(([a], [b]) => a.localeCompare(b));
2348
- const tasks = await Promise.all(entries.map(async ([key, meta]) => {
2349
- const logFile = meta.logFile ?? import_node_path9.default.join(logsDir, key);
3397
+ const aliveEntries = [];
3398
+ for (const [key, meta] of entries) {
3399
+ const pid = typeof meta.pid === "number" ? meta.pid : void 0;
3400
+ if (pid && !isProcessAlive(pid)) {
3401
+ const logFile = meta.logFile ?? import_node_path11.default.join(logsDir, key);
3402
+ await safeRemoveRegistry(logFile);
3403
+ continue;
3404
+ }
3405
+ aliveEntries.push([key, meta]);
3406
+ }
3407
+ const tasks = await Promise.all(aliveEntries.map(async ([key, meta]) => {
3408
+ const logFile = meta.logFile ?? import_node_path11.default.join(logsDir, key);
2350
3409
  const lines = await readLogLines2(logFile);
2351
3410
  return {
2352
3411
  key,
@@ -2357,56 +3416,71 @@ async function loadTasks(logsDir) {
2357
3416
  }));
2358
3417
  return tasks;
2359
3418
  }
2360
- function buildHeader(state, columns) {
3419
+ function buildHeader2(state, columns) {
2361
3420
  if (state.tasks.length === 0) {
2362
- return truncateLine2("\u6682\u65E0\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\uFF0C\u6309 q \u9000\u51FA", columns);
3421
+ return truncateLine3("\u6682\u65E0\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\uFF0C\u6309 q \u9000\u51FA", columns);
2363
3422
  }
2364
3423
  const current = state.tasks[state.selectedIndex];
2365
3424
  const total = state.tasks.length;
2366
3425
  const index = state.selectedIndex + 1;
2367
- const title = `\u4EFB\u52A1 ${index}/${total} \uFF5C ${current.key} \uFF5C \u2190/\u2192 \u5207\u6362\u4EFB\u52A1 \u2191/\u2193 \u7FFB\u9875 q \u9000\u51FA`;
2368
- return truncateLine2(title, columns);
3426
+ const title = `\u4EFB\u52A1 ${index}/${total} \uFF5C ${current.key} \uFF5C \u2190/\u2192 \u5207\u6362\u4EFB\u52A1 \u2191/\u2193 \u4E0A\u4E0B 1 \u884C PageUp/PageDown \u7FFB\u9875 t \u7EC8\u6B62 q \u9000\u51FA`;
3427
+ return truncateLine3(title, columns);
2369
3428
  }
2370
- function buildStatus(task, page, columns, errorMessage) {
3429
+ function buildStatus2(task, page, columns, errorMessage, statusMessage, statusIsError) {
2371
3430
  const meta = task.meta;
2372
3431
  const status = `\u8F6E\u6B21 ${meta.round} \uFF5C Token ${meta.tokenUsed} \uFF5C \u9875 ${page.current}/${page.total} \uFF5C \u9879\u76EE ${meta.path}`;
2373
- const suffix = errorMessage ? ` \uFF5C \u5237\u65B0\u5931\u8D25\uFF1A${errorMessage}` : "";
2374
- return truncateLine2(`${status}${suffix}`, columns);
3432
+ const extras = [];
3433
+ if (errorMessage) {
3434
+ extras.push(`\u5237\u65B0\u5931\u8D25\uFF1A${errorMessage}`);
3435
+ }
3436
+ if (statusMessage) {
3437
+ extras.push(statusIsError ? `\u64CD\u4F5C\u5931\u8D25\uFF1A${statusMessage}` : statusMessage);
3438
+ }
3439
+ const suffix = extras.length > 0 ? ` \uFF5C ${extras.join(" \uFF5C ")}` : "";
3440
+ return truncateLine3(`${status}${suffix}`, columns);
2375
3441
  }
2376
- function getPageSize2(rows) {
3442
+ function getPageSize3(rows) {
2377
3443
  return Math.max(1, rows - 2);
2378
3444
  }
2379
- function render2(state) {
2380
- const { rows, columns } = getTerminalSize2();
2381
- const pageSize = getPageSize2(rows);
2382
- const header = buildHeader(state, columns);
3445
+ function render3(state) {
3446
+ const { rows, columns } = getTerminalSize3();
3447
+ const pageSize = getPageSize3(rows);
3448
+ const header = buildHeader2(state, columns);
2383
3449
  if (state.tasks.length === 0) {
2384
3450
  const filler = Array.from({ length: pageSize }, () => "");
2385
- const statusText = state.lastError ? `\u5237\u65B0\u5931\u8D25\uFF1A${state.lastError}` : "\u7B49\u5F85\u540E\u53F0\u4EFB\u52A1\u542F\u52A8\u2026";
2386
- const status2 = truncateLine2(statusText, columns);
3451
+ const statusText = state.lastError ? `\u5237\u65B0\u5931\u8D25\uFF1A${state.lastError}` : state.statusMessage ? state.statusIsError ? `\u64CD\u4F5C\u5931\u8D25\uFF1A${state.statusMessage}` : state.statusMessage : "\u7B49\u5F85\u540E\u53F0\u4EFB\u52A1\u542F\u52A8\u2026";
3452
+ const status2 = truncateLine3(statusText, columns);
2387
3453
  const content2 = [header, ...filler, status2].join("\n");
2388
3454
  process.stdout.write(`\x1B[2J\x1B[H${content2}`);
2389
3455
  return;
2390
3456
  }
2391
3457
  const current = state.tasks[state.selectedIndex];
2392
3458
  const lines = current.lines;
2393
- const maxOffset = Math.max(0, Math.ceil(lines.length / pageSize) - 1);
2394
- const offset = state.pageOffsets.get(current.key) ?? maxOffset;
3459
+ const maxOffset = Math.max(0, lines.length - pageSize);
3460
+ const offset = state.lineOffsets.get(current.key) ?? maxOffset;
2395
3461
  const stick = state.stickToBottom.get(current.key) ?? true;
2396
3462
  const nextOffset = Math.min(Math.max(stick ? maxOffset : offset, 0), maxOffset);
2397
- state.pageOffsets.set(current.key, nextOffset);
3463
+ state.lineOffsets.set(current.key, nextOffset);
2398
3464
  state.stickToBottom.set(current.key, nextOffset === maxOffset);
2399
- const start = nextOffset * pageSize;
2400
- const pageLines = lines.slice(start, start + pageSize).map((line) => truncateLine2(line, columns));
3465
+ const start = nextOffset;
3466
+ const pageLines = lines.slice(start, start + pageSize).map((line) => truncateLine3(line, columns));
2401
3467
  while (pageLines.length < pageSize) {
2402
3468
  pageLines.push("");
2403
3469
  }
2404
- const status = buildStatus(
3470
+ const totalPages = Math.max(1, Math.ceil(lines.length / pageSize));
3471
+ const currentPage = Math.min(totalPages, Math.floor(nextOffset / pageSize) + 1);
3472
+ const status = buildStatus2(
2405
3473
  current,
2406
- { current: nextOffset + 1, total: Math.max(1, maxOffset + 1) },
3474
+ { current: currentPage, total: totalPages },
2407
3475
  columns,
2408
- state.lastError
3476
+ state.lastError,
3477
+ state.statusMessage,
3478
+ state.statusIsError
2409
3479
  );
3480
+ if (state.confirm) {
3481
+ const dialogLines = buildConfirmDialogLines(state.confirm.key, columns);
3482
+ applyDialogOverlay(pageLines, dialogLines);
3483
+ }
2410
3484
  const content = [header, ...pageLines, status].join("\n");
2411
3485
  process.stdout.write(`\x1B[2J\x1B[H${content}`);
2412
3486
  }
@@ -2429,9 +3503,9 @@ function updateSelection(state, tasks) {
2429
3503
  }
2430
3504
  state.selectedKey = tasks[state.selectedIndex]?.key;
2431
3505
  const existing = new Set(tasks.map((task) => task.key));
2432
- for (const key of state.pageOffsets.keys()) {
3506
+ for (const key of state.lineOffsets.keys()) {
2433
3507
  if (!existing.has(key)) {
2434
- state.pageOffsets.delete(key);
3508
+ state.lineOffsets.delete(key);
2435
3509
  }
2436
3510
  }
2437
3511
  for (const key of state.stickToBottom.keys()) {
@@ -2439,6 +3513,9 @@ function updateSelection(state, tasks) {
2439
3513
  state.stickToBottom.delete(key);
2440
3514
  }
2441
3515
  }
3516
+ if (state.confirm && !existing.has(state.confirm.key)) {
3517
+ state.confirm = void 0;
3518
+ }
2442
3519
  }
2443
3520
  function moveSelection(state, direction) {
2444
3521
  if (state.tasks.length === 0) return;
@@ -2446,24 +3523,92 @@ function moveSelection(state, direction) {
2446
3523
  state.selectedIndex = (state.selectedIndex + direction + total) % total;
2447
3524
  state.selectedKey = state.tasks[state.selectedIndex]?.key;
2448
3525
  }
2449
- function movePage(state, direction) {
3526
+ function moveLine(state, direction) {
2450
3527
  if (state.tasks.length === 0) return;
2451
- const { rows } = getTerminalSize2();
2452
- const pageSize = getPageSize2(rows);
2453
3528
  const current = state.tasks[state.selectedIndex];
2454
3529
  const lines = current.lines;
2455
- const maxOffset = Math.max(0, Math.ceil(lines.length / pageSize) - 1);
2456
- const offset = state.pageOffsets.get(current.key) ?? maxOffset;
3530
+ const { rows } = getTerminalSize3();
3531
+ const pageSize = getPageSize3(rows);
3532
+ const maxOffset = Math.max(0, lines.length - pageSize);
3533
+ const offset = state.lineOffsets.get(current.key) ?? maxOffset;
2457
3534
  const nextOffset = Math.min(Math.max(offset + direction, 0), maxOffset);
2458
- state.pageOffsets.set(current.key, nextOffset);
3535
+ state.lineOffsets.set(current.key, nextOffset);
2459
3536
  state.stickToBottom.set(current.key, nextOffset === maxOffset);
2460
3537
  }
2461
- function shouldExit2(input) {
3538
+ function movePage(state, direction) {
3539
+ if (state.tasks.length === 0) return;
3540
+ const { rows } = getTerminalSize3();
3541
+ const pageSize = getPageSize3(rows);
3542
+ const current = state.tasks[state.selectedIndex];
3543
+ const lines = current.lines;
3544
+ const maxOffset = Math.max(0, lines.length - pageSize);
3545
+ const offset = state.lineOffsets.get(current.key) ?? maxOffset;
3546
+ const nextOffset = Math.min(Math.max(offset + direction * pageSize, 0), maxOffset);
3547
+ state.lineOffsets.set(current.key, nextOffset);
3548
+ state.stickToBottom.set(current.key, nextOffset === maxOffset);
3549
+ }
3550
+ async function terminateTask(task) {
3551
+ const pid = typeof task.meta.pid === "number" ? task.meta.pid : void 0;
3552
+ if (!pid || pid <= 0) {
3553
+ return { message: "\u4EFB\u52A1\u672A\u8BB0\u5F55 PID\uFF0C\u65E0\u6CD5\u7EC8\u6B62", isError: true, removed: false };
3554
+ }
3555
+ const target = resolveTerminationTarget(pid);
3556
+ try {
3557
+ process.kill(target, "SIGTERM");
3558
+ } catch (error) {
3559
+ const err = error;
3560
+ if (err?.code === "ESRCH") {
3561
+ await safeRemoveRegistry(task.logFile);
3562
+ return { message: `\u4EFB\u52A1 ${task.key} \u5DF2\u7ED3\u675F`, isError: false, removed: true };
3563
+ }
3564
+ const message = error instanceof Error ? error.message : String(error);
3565
+ return { message: `\u53D1\u9001\u7EC8\u6B62\u4FE1\u53F7\u5931\u8D25\uFF1A${message}`, isError: true, removed: false };
3566
+ }
3567
+ await sleep(TERMINATE_GRACE_MS);
3568
+ if (!isProcessAlive(pid)) {
3569
+ await safeRemoveRegistry(task.logFile);
3570
+ return { message: `\u4EFB\u52A1 ${task.key} \u5DF2\u7EC8\u6B62`, isError: false, removed: true };
3571
+ }
3572
+ return { message: `\u5DF2\u53D1\u9001\u7EC8\u6B62\u4FE1\u53F7\uFF0C\u4EFB\u52A1\u4ECD\u5728\u8FD0\u884C`, isError: false, removed: false };
3573
+ }
3574
+ async function terminateTaskByKey(state, key, refresh) {
3575
+ const task = findTaskByKey(state, key);
3576
+ if (!task) {
3577
+ setStatus(state, `\u4EFB\u52A1 ${key} \u5DF2\u4E0D\u5B58\u5728`, true);
3578
+ render3(state);
3579
+ return;
3580
+ }
3581
+ const result = await terminateTask(task);
3582
+ setStatus(state, result.message, result.isError);
3583
+ if (result.removed) {
3584
+ await refresh();
3585
+ render3(state);
3586
+ return;
3587
+ }
3588
+ render3(state);
3589
+ }
3590
+ function shouldExit3(input) {
2462
3591
  if (input === "") return true;
2463
3592
  if (input.toLowerCase() === "q") return true;
2464
3593
  return false;
2465
3594
  }
2466
- function handleInput(state, input) {
3595
+ function handleInput(state, input, refresh) {
3596
+ const lower = input.toLowerCase();
3597
+ if (state.confirm) {
3598
+ if (lower.includes("y")) {
3599
+ const key = state.confirm.key;
3600
+ state.confirm = void 0;
3601
+ setStatus(state, `\u6B63\u5728\u7EC8\u6B62\u4EFB\u52A1 ${key}...`);
3602
+ void terminateTaskByKey(state, key, refresh);
3603
+ return;
3604
+ }
3605
+ if (lower.includes("n") || input === "\x1B") {
3606
+ state.confirm = void 0;
3607
+ setStatus(state, "\u5DF2\u53D6\u6D88\u7EC8\u6B62");
3608
+ return;
3609
+ }
3610
+ return;
3611
+ }
2467
3612
  if (input.includes("\x1B[D")) {
2468
3613
  moveSelection(state, -1);
2469
3614
  return;
@@ -2473,15 +3618,36 @@ function handleInput(state, input) {
2473
3618
  return;
2474
3619
  }
2475
3620
  if (input.includes("\x1B[A")) {
2476
- movePage(state, -1);
3621
+ moveLine(state, -1);
2477
3622
  return;
2478
3623
  }
2479
3624
  if (input.includes("\x1B[B")) {
3625
+ moveLine(state, 1);
3626
+ return;
3627
+ }
3628
+ if (input.includes("\x1B[5~")) {
3629
+ movePage(state, -1);
3630
+ return;
3631
+ }
3632
+ if (input.includes("\x1B[6~")) {
2480
3633
  movePage(state, 1);
2481
3634
  return;
2482
3635
  }
3636
+ if (lower.includes("t")) {
3637
+ if (state.tasks.length === 0) {
3638
+ setStatus(state, "\u6682\u65E0\u53EF\u7EC8\u6B62\u7684\u4EFB\u52A1", true);
3639
+ return;
3640
+ }
3641
+ const current = state.tasks[state.selectedIndex];
3642
+ if (typeof current.meta.pid !== "number" || current.meta.pid <= 0) {
3643
+ setStatus(state, "\u4EFB\u52A1\u672A\u8BB0\u5F55 PID\uFF0C\u65E0\u6CD5\u7EC8\u6B62", true);
3644
+ return;
3645
+ }
3646
+ state.confirm = { key: current.key };
3647
+ setStatus(state, void 0);
3648
+ }
2483
3649
  }
2484
- function setupCleanup2(cleanup) {
3650
+ function setupCleanup3(cleanup) {
2485
3651
  const exitHandler = () => {
2486
3652
  cleanup();
2487
3653
  };
@@ -2502,7 +3668,7 @@ async function runMonitor() {
2502
3668
  const state = {
2503
3669
  tasks: [],
2504
3670
  selectedIndex: 0,
2505
- pageOffsets: /* @__PURE__ */ new Map(),
3671
+ lineOffsets: /* @__PURE__ */ new Map(),
2506
3672
  stickToBottom: /* @__PURE__ */ new Map()
2507
3673
  };
2508
3674
  let cleaned = false;
@@ -2515,7 +3681,7 @@ async function runMonitor() {
2515
3681
  }
2516
3682
  process.stdout.write("\x1B[?25h");
2517
3683
  };
2518
- setupCleanup2(cleanup);
3684
+ setupCleanup3(cleanup);
2519
3685
  process.stdout.write("\x1B[?25l");
2520
3686
  process.stdin.setRawMode(true);
2521
3687
  process.stdin.resume();
@@ -2527,11 +3693,11 @@ async function runMonitor() {
2527
3693
  const tasks = await loadTasks(logsDir);
2528
3694
  state.lastError = void 0;
2529
3695
  updateSelection(state, tasks);
2530
- render2(state);
3696
+ render3(state);
2531
3697
  } catch (error) {
2532
3698
  const message = error instanceof Error ? error.message : String(error);
2533
3699
  state.lastError = message;
2534
- render2(state);
3700
+ render3(state);
2535
3701
  } finally {
2536
3702
  refreshing = false;
2537
3703
  }
@@ -2540,20 +3706,102 @@ async function runMonitor() {
2540
3706
  const timer = setInterval(refresh, REFRESH_INTERVAL);
2541
3707
  process.stdin.on("data", (data) => {
2542
3708
  const input = data.toString("utf8");
2543
- if (shouldExit2(input)) {
3709
+ if (shouldExit3(input)) {
2544
3710
  clearInterval(timer);
2545
3711
  cleanup();
2546
3712
  process.exit(0);
2547
3713
  }
2548
- handleInput(state, input);
2549
- render2(state);
3714
+ handleInput(state, input, refresh);
3715
+ render3(state);
2550
3716
  });
2551
3717
  process.stdout.on("resize", () => {
2552
- render2(state);
3718
+ render3(state);
2553
3719
  });
2554
3720
  }
2555
3721
 
3722
+ // src/log-tailer.ts
3723
+ var import_promises = require("fs/promises");
3724
+ var import_fs_extra11 = __toESM(require("fs-extra"));
3725
+ function normalizeChunk(chunk) {
3726
+ return chunk.replace(/\r\n?/g, "\n");
3727
+ }
3728
+ async function tailLogFile(options) {
3729
+ const intervalMs = options.pollIntervalMs ?? 200;
3730
+ let offset = 0;
3731
+ let buffer = "";
3732
+ let reading = false;
3733
+ let stopped = false;
3734
+ if (options.startFromEnd) {
3735
+ try {
3736
+ const stat = await import_fs_extra11.default.stat(options.filePath);
3737
+ offset = stat.size;
3738
+ } catch {
3739
+ offset = 0;
3740
+ }
3741
+ }
3742
+ const flushBuffer = () => {
3743
+ if (!buffer) return;
3744
+ options.onLine(buffer);
3745
+ buffer = "";
3746
+ };
3747
+ const emitChunk = (chunk) => {
3748
+ buffer += normalizeChunk(chunk);
3749
+ const parts = buffer.split("\n");
3750
+ buffer = parts.pop() ?? "";
3751
+ for (const line of parts) {
3752
+ options.onLine(line);
3753
+ }
3754
+ };
3755
+ const readNew = async () => {
3756
+ if (reading || stopped) return;
3757
+ reading = true;
3758
+ try {
3759
+ const stat = await import_fs_extra11.default.stat(options.filePath);
3760
+ if (stat.size < offset) {
3761
+ offset = stat.size;
3762
+ buffer = "";
3763
+ }
3764
+ if (stat.size > offset) {
3765
+ const length = stat.size - offset;
3766
+ const handle = await (0, import_promises.open)(options.filePath, "r");
3767
+ try {
3768
+ const payload = Buffer.alloc(length);
3769
+ const result = await handle.read(payload, 0, length, offset);
3770
+ offset += result.bytesRead;
3771
+ if (result.bytesRead > 0) {
3772
+ const text = payload.subarray(0, result.bytesRead).toString("utf8");
3773
+ emitChunk(text);
3774
+ }
3775
+ } finally {
3776
+ await handle.close();
3777
+ }
3778
+ }
3779
+ } catch (error) {
3780
+ const err = error;
3781
+ if (err?.code !== "ENOENT") {
3782
+ const message = err instanceof Error ? err.message : String(err);
3783
+ options.onError?.(message);
3784
+ }
3785
+ } finally {
3786
+ reading = false;
3787
+ }
3788
+ };
3789
+ const timer = setInterval(() => {
3790
+ void readNew();
3791
+ }, intervalMs);
3792
+ await readNew();
3793
+ return {
3794
+ stop: async () => {
3795
+ if (stopped) return;
3796
+ stopped = true;
3797
+ clearInterval(timer);
3798
+ flushBuffer();
3799
+ }
3800
+ };
3801
+ }
3802
+
2556
3803
  // src/cli.ts
3804
+ var FOREGROUND_CHILD_ENV = "WHEEL_AI_FOREGROUND_CHILD";
2557
3805
  function parseInteger(value, defaultValue) {
2558
3806
  const parsed = Number.parseInt(value, 10);
2559
3807
  if (Number.isNaN(parsed)) return defaultValue;
@@ -2581,30 +3829,132 @@ function buildBackgroundArgs(argv, logFile, branchName, injectBranch = false) {
2581
3829
  }
2582
3830
  return filtered;
2583
3831
  }
3832
+ function extractAliasCommandArgs(argv, name) {
3833
+ const args = argv.slice(2);
3834
+ const start = args.findIndex((arg, index) => arg === "set" && args[index + 1] === "alias" && args[index + 2] === name);
3835
+ if (start < 0) return [];
3836
+ const rest = args.slice(start + 3);
3837
+ if (rest[0] === "--") return rest.slice(1);
3838
+ return rest;
3839
+ }
3840
+ async function runForegroundWithDetach(options) {
3841
+ const args = buildBackgroundArgs(options.argv, options.logFile, options.branchName, options.injectBranch);
3842
+ const child = (0, import_node_child_process.spawn)(process.execPath, [...process.execArgv, ...args], {
3843
+ detached: true,
3844
+ stdio: "ignore",
3845
+ env: {
3846
+ ...process.env,
3847
+ [FOREGROUND_CHILD_ENV]: "1"
3848
+ }
3849
+ });
3850
+ child.unref();
3851
+ const resolvedLogFile = resolvePath(process.cwd(), options.logFile);
3852
+ const existed = await import_fs_extra12.default.pathExists(resolvedLogFile);
3853
+ const tailer = await tailLogFile({
3854
+ filePath: resolvedLogFile,
3855
+ startFromEnd: existed,
3856
+ onLine: (line) => {
3857
+ process.stdout.write(`${line}
3858
+ `);
3859
+ },
3860
+ onError: (message) => {
3861
+ defaultLogger.warn(`\u65E5\u5FD7\u8BFB\u53D6\u5931\u8D25\uFF1A${message}`);
3862
+ }
3863
+ });
3864
+ const suffixNote = options.isMultiTask ? "\uFF08\u591A\u4EFB\u52A1\u5C06\u8FFD\u52A0\u5E8F\u53F7\uFF09" : "";
3865
+ console.log(`\u5DF2\u8FDB\u5165\u524D\u53F0\u65E5\u5FD7\u67E5\u770B\uFF0C\u6309 Esc \u5207\u5230\u540E\u53F0\u8FD0\u884C\uFF0C\u65E5\u5FD7\u8F93\u51FA\u81F3 ${resolvedLogFile}${suffixNote}`);
3866
+ let cleaned = false;
3867
+ const cleanup = async () => {
3868
+ if (cleaned) return;
3869
+ cleaned = true;
3870
+ await tailer.stop();
3871
+ if (process.stdin.isTTY) {
3872
+ process.stdin.setRawMode(false);
3873
+ process.stdin.pause();
3874
+ }
3875
+ };
3876
+ const detach = async () => {
3877
+ await cleanup();
3878
+ console.log(`\u5DF2\u5207\u5165\u540E\u53F0\u8FD0\u884C\uFF0C\u65E5\u5FD7\u8F93\u51FA\u81F3 ${resolvedLogFile}${suffixNote}`);
3879
+ process.exit(0);
3880
+ };
3881
+ const terminate = async () => {
3882
+ if (child.pid) {
3883
+ try {
3884
+ const target = resolveTerminationTarget(child.pid);
3885
+ process.kill(target, "SIGTERM");
3886
+ } catch (error) {
3887
+ const message = error instanceof Error ? error.message : String(error);
3888
+ defaultLogger.warn(`\u7EC8\u6B62\u5B50\u8FDB\u7A0B\u5931\u8D25\uFF1A${message}`);
3889
+ }
3890
+ }
3891
+ await cleanup();
3892
+ process.exit(0);
3893
+ };
3894
+ if (process.stdin.isTTY) {
3895
+ process.stdin.setRawMode(true);
3896
+ process.stdin.resume();
3897
+ process.stdin.on("data", (data) => {
3898
+ const input = data.toString("utf8");
3899
+ if (input === "\x1B") {
3900
+ void detach();
3901
+ return;
3902
+ }
3903
+ if (input === "") {
3904
+ void terminate();
3905
+ }
3906
+ });
3907
+ }
3908
+ process.on("SIGINT", () => {
3909
+ void terminate();
3910
+ });
3911
+ process.on("SIGTERM", () => {
3912
+ void terminate();
3913
+ });
3914
+ child.on("exit", async (code) => {
3915
+ await cleanup();
3916
+ process.exit(code ?? 0);
3917
+ });
3918
+ }
2584
3919
  async function runCli(argv) {
2585
3920
  const globalConfig = await loadGlobalConfig(defaultLogger);
2586
3921
  const effectiveArgv = applyShortcutArgv(argv, globalConfig);
2587
3922
  const program = new import_commander.Command();
2588
3923
  program.name("wheel-ai").description("\u57FA\u4E8E AI CLI \u7684\u6301\u7EED\u8FED\u4EE3\u5F00\u53D1\u5DE5\u5177").version("1.0.0");
2589
- program.command("run").requiredOption("-t, --task <task>", "\u9700\u8981\u5B8C\u6210\u7684\u4EFB\u52A1\u63CF\u8FF0\uFF08\u4F1A\u8FDB\u5165 AI \u63D0\u793A\uFF09").option("-i, --iterations <number>", "\u6700\u5927\u8FED\u4EE3\u6B21\u6570", (value) => parseInteger(value, 5), 5).option("--ai-cli <command>", "AI CLI \u547D\u4EE4", "claude").option("--ai-args <args...>", "AI CLI \u53C2\u6570", []).option("--ai-prompt-arg <flag>", "\u7528\u4E8E\u4F20\u5165 prompt \u7684\u53C2\u6570\uFF08\u4E3A\u7A7A\u5219\u4F7F\u7528 stdin\uFF09").option("--notes-file <path>", "\u6301\u4E45\u5316\u8BB0\u5FC6\u6587\u4EF6", defaultNotesPath()).option("--plan-file <path>", "\u8BA1\u5212\u6587\u4EF6", defaultPlanPath()).option("--workflow-doc <path>", "AI \u5DE5\u4F5C\u6D41\u7A0B\u8BF4\u660E\u6587\u4EF6", defaultWorkflowDoc()).option("--worktree", "\u5728\u72EC\u7ACB worktree \u4E0A\u6267\u884C", false).option("--branch <name>", "worktree \u5206\u652F\u540D\uFF08\u9ED8\u8BA4\u81EA\u52A8\u751F\u6210\u6216\u5F53\u524D\u5206\u652F\uFF09").option("--worktree-path <path>", "worktree \u8DEF\u5F84\uFF0C\u9ED8\u8BA4 ../worktrees/<branch>").option("--base-branch <name>", "\u521B\u5EFA\u5206\u652F\u7684\u57FA\u7EBF\u5206\u652F", "main").option("--skip-install", "\u8DF3\u8FC7\u5F00\u59CB\u4EFB\u52A1\u524D\u7684\u4F9D\u8D56\u68C0\u67E5", false).option("--run-tests", "\u8FD0\u884C\u5355\u5143\u6D4B\u8BD5\u547D\u4EE4", false).option("--run-e2e", "\u8FD0\u884C e2e \u6D4B\u8BD5\u547D\u4EE4", false).option("--unit-command <cmd>", "\u5355\u5143\u6D4B\u8BD5\u547D\u4EE4", "yarn test").option("--e2e-command <cmd>", "e2e \u6D4B\u8BD5\u547D\u4EE4", "yarn e2e").option("--auto-commit", "\u81EA\u52A8 git commit", false).option("--auto-push", "\u81EA\u52A8 git push", false).option("--pr", "\u4F7F\u7528 gh \u521B\u5EFA PR", false).option("--pr-title <title>", "PR \u6807\u9898").option("--pr-body <path>", "PR \u63CF\u8FF0\u6587\u4EF6\u8DEF\u5F84\uFF08\u53EF\u7559\u7A7A\u81EA\u52A8\u751F\u6210\uFF09").option("--draft", "\u4EE5\u8349\u7A3F\u5F62\u5F0F\u521B\u5EFA PR", false).option("--reviewer <user...>", "PR reviewers", collect, []).option("--webhook <url>", "webhook \u901A\u77E5 URL\uFF08\u53EF\u91CD\u590D\uFF09", collect, []).option("--webhook-timeout <ms>", "webhook \u8BF7\u6C42\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09", (value) => parseInteger(value, 8e3)).option("--stop-signal <token>", "AI \u8F93\u51FA\u4E2D\u7684\u505C\u6B62\u6807\u8BB0", "<<DONE>>").option("--log-file <path>", "\u65E5\u5FD7\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84").option("--background", "\u5207\u5165\u540E\u53F0\u8FD0\u884C", false).option("-v, --verbose", "\u8F93\u51FA\u8C03\u8BD5\u65E5\u5FD7", false).action(async (options) => {
3924
+ program.command("run").option("-t, --task <task>", "\u9700\u8981\u5B8C\u6210\u7684\u4EFB\u52A1\u63CF\u8FF0\uFF08\u53EF\u91CD\u590D\u4F20\u5165\uFF0C\u72EC\u7ACB\u5904\u7406\uFF09", collect, []).option("-i, --iterations <number>", "\u6700\u5927\u8FED\u4EE3\u6B21\u6570", (value) => parseInteger(value, 5), 5).option("--ai-cli <command>", "AI CLI \u547D\u4EE4", "claude").option("--ai-args <args...>", "AI CLI \u53C2\u6570", []).option("--ai-prompt-arg <flag>", "\u7528\u4E8E\u4F20\u5165 prompt \u7684\u53C2\u6570\uFF08\u4E3A\u7A7A\u5219\u4F7F\u7528 stdin\uFF09").option("--notes-file <path>", "\u6301\u4E45\u5316\u8BB0\u5FC6\u6587\u4EF6", defaultNotesPath()).option("--plan-file <path>", "\u8BA1\u5212\u6587\u4EF6", defaultPlanPath()).option("--workflow-doc <path>", "AI \u5DE5\u4F5C\u6D41\u7A0B\u8BF4\u660E\u6587\u4EF6", defaultWorkflowDoc()).option("--worktree", "\u5728\u72EC\u7ACB worktree \u4E0A\u6267\u884C", false).option("--branch <name>", "worktree \u5206\u652F\u540D\uFF08\u9ED8\u8BA4\u81EA\u52A8\u751F\u6210\u6216\u5F53\u524D\u5206\u652F\uFF09").option("--worktree-path <path>", "worktree \u8DEF\u5F84\uFF0C\u9ED8\u8BA4 ../worktrees/<branch>").option("--base-branch <name>", "\u521B\u5EFA\u5206\u652F\u7684\u57FA\u7EBF\u5206\u652F", "main").option("--skip-install", "\u8DF3\u8FC7\u5F00\u59CB\u4EFB\u52A1\u524D\u7684\u4F9D\u8D56\u68C0\u67E5", false).option("--run-tests", "\u8FD0\u884C\u5355\u5143\u6D4B\u8BD5\u547D\u4EE4", false).option("--run-e2e", "\u8FD0\u884C e2e \u6D4B\u8BD5\u547D\u4EE4", false).option("--unit-command <cmd>", "\u5355\u5143\u6D4B\u8BD5\u547D\u4EE4", "yarn test").option("--e2e-command <cmd>", "e2e \u6D4B\u8BD5\u547D\u4EE4", "yarn e2e").option("--auto-commit", "\u81EA\u52A8 git commit", false).option("--auto-push", "\u81EA\u52A8 git push", false).option("--pr", "\u4F7F\u7528 gh \u521B\u5EFA PR", false).option("--pr-title <title>", "PR \u6807\u9898").option("--pr-body <path>", "PR \u63CF\u8FF0\u6587\u4EF6\u8DEF\u5F84\uFF08\u53EF\u7559\u7A7A\u81EA\u52A8\u751F\u6210\uFF09").option("--draft", "\u4EE5\u8349\u7A3F\u5F62\u5F0F\u521B\u5EFA PR", false).option("--reviewer <user...>", "PR reviewers", collect, []).option("--auto-merge", "PR \u68C0\u67E5\u901A\u8FC7\u540E\u81EA\u52A8\u5408\u5E76", false).option("--webhook <url>", "webhook \u901A\u77E5 URL\uFF08\u53EF\u91CD\u590D\uFF09", collect, []).option("--webhook-timeout <ms>", "webhook \u8BF7\u6C42\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09", (value) => parseInteger(value, 8e3)).option("--multi-task-mode <mode>", "\u591A\u4EFB\u52A1\u6267\u884C\u6A21\u5F0F\uFF08relay/serial/serial-continue/parallel\uFF0C\u6216\u4E2D\u6587\u63CF\u8FF0\uFF09", "relay").option("--stop-signal <token>", "AI \u8F93\u51FA\u4E2D\u7684\u505C\u6B62\u6807\u8BB0", "<<DONE>>").option("--log-file <path>", "\u65E5\u5FD7\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84").option("--background", "\u5207\u5165\u540E\u53F0\u8FD0\u884C", false).option("-v, --verbose", "\u8F93\u51FA\u8C03\u8BD5\u65E5\u5FD7", false).option("--skip-quality", "\u8DF3\u8FC7\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5", false).action(async (options) => {
3925
+ const tasks = normalizeTaskList(options.task);
3926
+ if (tasks.length === 0) {
3927
+ throw new Error("\u9700\u8981\u81F3\u5C11\u63D0\u4F9B\u4E00\u4E2A\u4EFB\u52A1\u63CF\u8FF0");
3928
+ }
3929
+ const multiTaskMode = parseMultiTaskMode(options.multiTaskMode);
2590
3930
  const useWorktree = Boolean(options.worktree);
3931
+ if (multiTaskMode === "parallel" && !useWorktree) {
3932
+ throw new Error("\u5E76\u884C\u6A21\u5F0F\u5FC5\u987B\u542F\u7528 --worktree");
3933
+ }
2591
3934
  const branchInput = normalizeOptional(options.branch);
2592
3935
  const logFileInput = normalizeOptional(options.logFile);
3936
+ const worktreePathInput = normalizeOptional(options.worktreePath);
2593
3937
  const background = Boolean(options.background);
2594
- let branchName = branchInput;
2595
- if (useWorktree && !branchName) {
2596
- branchName = generateBranchName();
2597
- }
3938
+ const isMultiTask = tasks.length > 1;
3939
+ const isForegroundChild = process.env[FOREGROUND_CHILD_ENV] === "1";
3940
+ const canForegroundDetach = !background && !isForegroundChild && process.stdout.isTTY && process.stdin.isTTY;
3941
+ const shouldInjectBranch = Boolean(useWorktree && branchInput && !isMultiTask);
3942
+ const branchNameForBackground = branchInput;
2598
3943
  let logFile = logFileInput;
2599
- if (background && !logFile) {
2600
- let branchForLog = branchName;
2601
- if (!branchForLog) {
2602
- try {
2603
- const current = await getCurrentBranch(process.cwd(), defaultLogger);
2604
- branchForLog = current || "detached";
2605
- } catch {
2606
- branchForLog = "unknown";
3944
+ if ((background || canForegroundDetach) && !logFile) {
3945
+ let branchForLog = "multi-task";
3946
+ if (!isMultiTask) {
3947
+ branchForLog = branchNameForBackground ?? "";
3948
+ if (!branchForLog) {
3949
+ try {
3950
+ const current = await getCurrentBranch(process.cwd(), defaultLogger);
3951
+ branchForLog = current || "detached";
3952
+ } catch {
3953
+ branchForLog = "unknown";
3954
+ }
2607
3955
  }
3956
+ } else if (branchInput) {
3957
+ branchForLog = `${branchInput}-multi`;
2608
3958
  }
2609
3959
  logFile = buildAutoLogFilePath(branchForLog);
2610
3960
  }
@@ -2612,18 +3962,40 @@ async function runCli(argv) {
2612
3962
  if (!logFile) {
2613
3963
  throw new Error("\u540E\u53F0\u8FD0\u884C\u9700\u8981\u6307\u5B9A\u65E5\u5FD7\u6587\u4EF6");
2614
3964
  }
2615
- const args = buildBackgroundArgs(effectiveArgv, logFile, branchName, useWorktree && !branchInput);
3965
+ const args = buildBackgroundArgs(effectiveArgv, logFile, branchNameForBackground, shouldInjectBranch);
2616
3966
  const child = (0, import_node_child_process.spawn)(process.execPath, [...process.execArgv, ...args], {
2617
3967
  detached: true,
2618
3968
  stdio: "ignore"
2619
3969
  });
2620
3970
  child.unref();
2621
3971
  const displayLogFile = resolvePath(process.cwd(), logFile);
2622
- console.log(`\u5DF2\u5207\u5165\u540E\u53F0\u8FD0\u884C\uFF0C\u65E5\u5FD7\u8F93\u51FA\u81F3 ${displayLogFile}`);
3972
+ const suffixNote = isMultiTask ? "\uFF08\u591A\u4EFB\u52A1\u5C06\u8FFD\u52A0\u5E8F\u53F7\uFF09" : "";
3973
+ console.log(`\u5DF2\u5207\u5165\u540E\u53F0\u8FD0\u884C\uFF0C\u65E5\u5FD7\u8F93\u51FA\u81F3 ${displayLogFile}${suffixNote}`);
2623
3974
  return;
2624
3975
  }
2625
- const cliOptions = {
2626
- task: options.task,
3976
+ if (canForegroundDetach) {
3977
+ if (!logFile) {
3978
+ throw new Error("\u5207\u5165\u540E\u53F0\u9700\u8981\u6307\u5B9A\u65E5\u5FD7\u6587\u4EF6");
3979
+ }
3980
+ await runForegroundWithDetach({
3981
+ argv: effectiveArgv,
3982
+ logFile,
3983
+ branchName: branchNameForBackground,
3984
+ injectBranch: shouldInjectBranch,
3985
+ isMultiTask
3986
+ });
3987
+ return;
3988
+ }
3989
+ const taskPlans = buildTaskPlans({
3990
+ tasks,
3991
+ mode: multiTaskMode,
3992
+ useWorktree,
3993
+ baseBranch: options.baseBranch,
3994
+ branchInput,
3995
+ worktreePath: worktreePathInput,
3996
+ logFile: logFileInput
3997
+ });
3998
+ const baseOptions = {
2627
3999
  iterations: options.iterations,
2628
4000
  aiCli: options.aiCli,
2629
4001
  aiArgs: options.aiArgs ?? [],
@@ -2632,9 +4004,6 @@ async function runCli(argv) {
2632
4004
  planFile: options.planFile,
2633
4005
  workflowDoc: options.workflowDoc,
2634
4006
  useWorktree,
2635
- branch: branchName,
2636
- worktreePath: options.worktreePath,
2637
- baseBranch: options.baseBranch,
2638
4007
  runTests: Boolean(options.runTests),
2639
4008
  runE2e: Boolean(options.runE2e),
2640
4009
  unitCommand: options.unitCommand,
@@ -2646,22 +4015,91 @@ async function runCli(argv) {
2646
4015
  prBody: options.prBody,
2647
4016
  draft: Boolean(options.draft),
2648
4017
  reviewers: options.reviewer ?? [],
4018
+ autoMerge: Boolean(options.autoMerge),
2649
4019
  webhookUrls: options.webhook ?? [],
2650
4020
  webhookTimeout: options.webhookTimeout,
2651
4021
  stopSignal: options.stopSignal,
2652
- logFile,
2653
4022
  verbose: Boolean(options.verbose),
2654
- skipInstall: Boolean(options.skipInstall)
4023
+ skipInstall: Boolean(options.skipInstall),
4024
+ skipQuality: Boolean(options.skipQuality)
2655
4025
  };
2656
- const config = buildLoopConfig(cliOptions, process.cwd());
2657
- await runLoop(config);
4026
+ const dynamicRelay = useWorktree && multiTaskMode === "relay" && !branchInput;
4027
+ let relayBaseBranch = options.baseBranch;
4028
+ const runPlan = async (plan, baseBranchOverride) => {
4029
+ const cliOptions = {
4030
+ task: plan.task,
4031
+ ...baseOptions,
4032
+ branch: plan.branchName,
4033
+ worktreePath: plan.worktreePath,
4034
+ baseBranch: baseBranchOverride ?? plan.baseBranch,
4035
+ logFile: plan.logFile
4036
+ };
4037
+ const config = buildLoopConfig(cliOptions, process.cwd());
4038
+ return runLoop(config);
4039
+ };
4040
+ if (multiTaskMode === "parallel") {
4041
+ const results = await Promise.allSettled(taskPlans.map((plan) => runPlan(plan)));
4042
+ const errors = results.flatMap((result, index) => {
4043
+ if (result.status === "fulfilled") return [];
4044
+ const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
4045
+ return [`\u4EFB\u52A1 ${index + 1} \u5931\u8D25: ${reason}`];
4046
+ });
4047
+ if (errors.length > 0) {
4048
+ errors.forEach((message) => defaultLogger.warn(message));
4049
+ throw new Error(errors.join("\n"));
4050
+ }
4051
+ return;
4052
+ }
4053
+ if (multiTaskMode === "serial-continue") {
4054
+ const errors = [];
4055
+ for (const plan of taskPlans) {
4056
+ try {
4057
+ const baseBranch = dynamicRelay ? relayBaseBranch : plan.baseBranch;
4058
+ const result = await runPlan(plan, baseBranch);
4059
+ if (dynamicRelay && result.branchName) {
4060
+ relayBaseBranch = result.branchName;
4061
+ }
4062
+ } catch (error) {
4063
+ const message = error instanceof Error ? error.message : String(error);
4064
+ errors.push(`\u4EFB\u52A1 ${plan.index + 1} \u5931\u8D25: ${message}`);
4065
+ defaultLogger.warn(`\u4EFB\u52A1 ${plan.index + 1} \u6267\u884C\u5931\u8D25\uFF0C\u7EE7\u7EED\u4E0B\u4E00\u4EFB\u52A1\uFF1A${message}`);
4066
+ }
4067
+ }
4068
+ if (errors.length > 0) {
4069
+ throw new Error(errors.join("\n"));
4070
+ }
4071
+ return;
4072
+ }
4073
+ for (const plan of taskPlans) {
4074
+ const baseBranch = dynamicRelay ? relayBaseBranch : plan.baseBranch;
4075
+ const result = await runPlan(plan, baseBranch);
4076
+ if (dynamicRelay && result.branchName) {
4077
+ relayBaseBranch = result.branchName;
4078
+ }
4079
+ }
2658
4080
  });
2659
- program.command("monitor").description("\u67E5\u770B\u540E\u53F0\u8FD0\u884C\u65E5\u5FD7").action(async () => {
4081
+ program.command("monitor").description("\u67E5\u770B\u540E\u53F0\u8FD0\u884C\u65E5\u5FD7\uFF08t \u7EC8\u6B62\u4EFB\u52A1\uFF09").action(async () => {
2660
4082
  await runMonitor();
2661
4083
  });
2662
4084
  program.command("logs").description("\u67E5\u770B\u5386\u53F2\u65E5\u5FD7").action(async () => {
2663
4085
  await runLogsViewer();
2664
4086
  });
4087
+ program.command("set").description("\u5199\u5165\u5168\u5C40\u914D\u7F6E").command("alias <name> [options...]").description("\u8BBE\u7F6E alias").allowUnknownOption(true).action(async (name) => {
4088
+ const normalized = normalizeAliasName(name);
4089
+ if (!normalized) {
4090
+ throw new Error("alias \u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A\u4E14\u4E0D\u80FD\u5305\u542B\u7A7A\u767D\u5B57\u7B26");
4091
+ }
4092
+ const commandArgs = extractAliasCommandArgs(effectiveArgv, name);
4093
+ const commandLine = formatCommandLine(commandArgs);
4094
+ if (!commandLine) {
4095
+ throw new Error("alias \u547D\u4EE4\u4E0D\u80FD\u4E3A\u7A7A");
4096
+ }
4097
+ await upsertAliasEntry(normalized, commandLine);
4098
+ console.log(`\u5DF2\u5199\u5165 alias\uFF1A${normalized}`);
4099
+ });
4100
+ program.command("alias").alias("aliases").description("\u6D4F\u89C8\u5168\u5C40 alias \u914D\u7F6E").action(async () => {
4101
+ await runAliasViewer();
4102
+ });
2665
4103
  await program.parseAsync(effectiveArgv);
2666
4104
  }
2667
4105
  if (require.main === module) {