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