@xdevops/issue-auto-finish 1.0.84 → 1.0.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/{LockNote-7OF7ADI2.js → LockNote-Z2CLDZNN.js} +3 -3
  2. package/dist/{ai-runner-MCGEQGXS.js → ai-runner-SVUNA3FX.js} +2 -2
  3. package/dist/{analyze-ZITN3CSH.js → analyze-SXXPE5XL.js} +3 -3
  4. package/dist/{braindump-LQU65XND.js → braindump-4E5SDMSZ.js} +6 -6
  5. package/dist/{chunk-7FLKETBC.js → chunk-4LFNFRCL.js} +13 -1
  6. package/dist/chunk-4LFNFRCL.js.map +1 -0
  7. package/dist/{chunk-HKI3BON6.js → chunk-4QV6D34Y.js} +3 -3
  8. package/dist/{chunk-7YCDMVIF.js → chunk-5UPYA6KH.js} +506 -216
  9. package/dist/chunk-5UPYA6KH.js.map +1 -0
  10. package/dist/{chunk-MV2CADMB.js → chunk-FWEW5E3B.js} +2 -2
  11. package/dist/{chunk-NZ7K73B7.js → chunk-GXFG4JU6.js} +2 -2
  12. package/dist/{chunk-3V3GQCB7.js → chunk-HDFNMVRQ.js} +2 -2
  13. package/dist/{chunk-3RNGPMRE.js → chunk-HOFYJEJ4.js} +7 -7
  14. package/dist/{chunk-PWA46LUR.js → chunk-JINMYD56.js} +3 -3
  15. package/dist/{chunk-WHFY274N.js → chunk-K2OTLYJI.js} +118 -17
  16. package/dist/chunk-K2OTLYJI.js.map +1 -0
  17. package/dist/{chunk-GDTS2J2P.js → chunk-KTYPZTF4.js} +2 -2
  18. package/dist/{chunk-6S7ERGQ7.js → chunk-P4O4ZXEC.js} +10 -5
  19. package/dist/chunk-P4O4ZXEC.js.map +1 -0
  20. package/dist/{chunk-KWODU7HB.js → chunk-YCYVNRLF.js} +15 -1
  21. package/dist/chunk-YCYVNRLF.js.map +1 -0
  22. package/dist/cli.js +8 -8
  23. package/dist/{config-C7AKWCPA.js → config-QLINHCHD.js} +3 -3
  24. package/dist/{doctor-P2ZH6PFX.js → doctor-37JNBGDN.js} +3 -3
  25. package/dist/errors/PhaseAbortedError.d.ts +13 -0
  26. package/dist/errors/PhaseAbortedError.d.ts.map +1 -0
  27. package/dist/errors/index.d.ts +1 -0
  28. package/dist/errors/index.d.ts.map +1 -1
  29. package/dist/events/EventBus.d.ts +1 -1
  30. package/dist/events/EventBus.d.ts.map +1 -1
  31. package/dist/i18n/locales/en.d.ts.map +1 -1
  32. package/dist/i18n/locales/zh-CN.d.ts.map +1 -1
  33. package/dist/index.js +11 -11
  34. package/dist/{init-D2BQIVVD.js → init-TDKDC6YP.js} +7 -7
  35. package/dist/lib.js +5 -5
  36. package/dist/lifecycle/ActionLifecycle.d.ts +1 -1
  37. package/dist/lifecycle/ActionLifecycle.d.ts.map +1 -1
  38. package/dist/lifecycle/ActionLifecycleManager.d.ts.map +1 -1
  39. package/dist/orchestrator/IssueProcessingContext.d.ts +2 -0
  40. package/dist/orchestrator/IssueProcessingContext.d.ts.map +1 -1
  41. package/dist/orchestrator/PipelineOrchestrator.d.ts +19 -0
  42. package/dist/orchestrator/PipelineOrchestrator.d.ts.map +1 -1
  43. package/dist/orchestrator/steps/FailureHandler.d.ts.map +1 -1
  44. package/dist/orchestrator/steps/PhaseLoopStep.d.ts.map +1 -1
  45. package/dist/phases/BasePhase.d.ts +10 -8
  46. package/dist/phases/BasePhase.d.ts.map +1 -1
  47. package/dist/phases/UatPhase.d.ts +32 -1
  48. package/dist/phases/UatPhase.d.ts.map +1 -1
  49. package/dist/pipeline/PipelineDefinition.d.ts.map +1 -1
  50. package/dist/prompts/templates.d.ts.map +1 -1
  51. package/dist/{restart-TP5RAFXZ.js → restart-RNXGTDWZ.js} +5 -5
  52. package/dist/run.js +11 -11
  53. package/dist/{start-KOBDZ2XN.js → start-27GRO4DP.js} +5 -5
  54. package/dist/tracker/IssueState.d.ts +4 -0
  55. package/dist/tracker/IssueState.d.ts.map +1 -1
  56. package/dist/tracker/IssueTracker.d.ts +2 -0
  57. package/dist/tracker/IssueTracker.d.ts.map +1 -1
  58. package/dist/webhook/CommandExecutor.d.ts +3 -0
  59. package/dist/webhook/CommandExecutor.d.ts.map +1 -1
  60. package/dist/webhook/CommandParser.d.ts +1 -1
  61. package/dist/webhook/CommandParser.d.ts.map +1 -1
  62. package/package.json +2 -1
  63. package/src/web/frontend/dist/assets/{index-GfpCL9Wn.js → index-C4NXoH9S.js} +55 -49
  64. package/src/web/frontend/dist/assets/{index-CPNbFsHB.css → index-C7lorIa0.css} +1 -1
  65. package/src/web/frontend/dist/index.html +2 -2
  66. package/dist/chunk-6S7ERGQ7.js.map +0 -1
  67. package/dist/chunk-7FLKETBC.js.map +0 -1
  68. package/dist/chunk-7YCDMVIF.js.map +0 -1
  69. package/dist/chunk-KWODU7HB.js.map +0 -1
  70. package/dist/chunk-WHFY274N.js.map +0 -1
  71. /package/dist/{LockNote-7OF7ADI2.js.map → LockNote-Z2CLDZNN.js.map} +0 -0
  72. /package/dist/{ai-runner-MCGEQGXS.js.map → ai-runner-SVUNA3FX.js.map} +0 -0
  73. /package/dist/{analyze-ZITN3CSH.js.map → analyze-SXXPE5XL.js.map} +0 -0
  74. /package/dist/{braindump-LQU65XND.js.map → braindump-4E5SDMSZ.js.map} +0 -0
  75. /package/dist/{chunk-HKI3BON6.js.map → chunk-4QV6D34Y.js.map} +0 -0
  76. /package/dist/{chunk-MV2CADMB.js.map → chunk-FWEW5E3B.js.map} +0 -0
  77. /package/dist/{chunk-NZ7K73B7.js.map → chunk-GXFG4JU6.js.map} +0 -0
  78. /package/dist/{chunk-3V3GQCB7.js.map → chunk-HDFNMVRQ.js.map} +0 -0
  79. /package/dist/{chunk-3RNGPMRE.js.map → chunk-HOFYJEJ4.js.map} +0 -0
  80. /package/dist/{chunk-PWA46LUR.js.map → chunk-JINMYD56.js.map} +0 -0
  81. /package/dist/{chunk-GDTS2J2P.js.map → chunk-KTYPZTF4.js.map} +0 -0
  82. /package/dist/{config-C7AKWCPA.js.map → config-QLINHCHD.js.map} +0 -0
  83. /package/dist/{doctor-P2ZH6PFX.js.map → doctor-37JNBGDN.js.map} +0 -0
  84. /package/dist/{init-D2BQIVVD.js.map → init-TDKDC6YP.js.map} +0 -0
  85. /package/dist/{restart-TP5RAFXZ.js.map → restart-RNXGTDWZ.js.map} +0 -0
  86. /package/dist/{start-KOBDZ2XN.js.map → start-27GRO4DP.js.map} +0 -0
@@ -17,13 +17,13 @@ import {
17
17
  planPrompt,
18
18
  rePlanPrompt,
19
19
  verifyPrompt
20
- } from "./chunk-6S7ERGQ7.js";
20
+ } from "./chunk-P4O4ZXEC.js";
21
21
  import {
22
22
  getProjectKnowledge
23
23
  } from "./chunk-ACVOOHAR.js";
24
24
  import {
25
25
  t
26
- } from "./chunk-KWODU7HB.js";
26
+ } from "./chunk-YCYVNRLF.js";
27
27
  import {
28
28
  getLocalIP
29
29
  } from "./chunk-AKXDQH25.js";
@@ -40,6 +40,7 @@ import {
40
40
  InvalidPhaseError,
41
41
  InvalidStateError,
42
42
  IssueNotFoundError,
43
+ PhaseAbortedError,
43
44
  PhaseNotRegisteredError,
44
45
  PipelineNotFoundError,
45
46
  PortExhaustionError,
@@ -48,7 +49,7 @@ import {
48
49
  createAIRunner,
49
50
  getRunnerCapabilities,
50
51
  isShuttingDown
51
- } from "./chunk-7FLKETBC.js";
52
+ } from "./chunk-4LFNFRCL.js";
52
53
  import {
53
54
  logger,
54
55
  runWithIssueContext
@@ -227,8 +228,8 @@ var GongfengClient = class {
227
228
  const encoded = encodeURIComponent(this.projectPath);
228
229
  return `${this.apiUrl}/api/v3/projects/${encoded}`;
229
230
  }
230
- async requestRaw(path14, options = {}) {
231
- const url = `${this.projectApiBase}${path14}`;
231
+ async requestRaw(path15, options = {}) {
232
+ const url = `${this.projectApiBase}${path15}`;
232
233
  logger4.debug("API request", { method: options.method || "GET", url });
233
234
  return this.circuitBreaker.execute(
234
235
  () => this.retryPolicy.execute(async () => {
@@ -245,11 +246,11 @@ var GongfengClient = class {
245
246
  throw new GongfengApiError(resp.status, `Gongfeng API error ${resp.status}: ${body}`, body);
246
247
  }
247
248
  return resp;
248
- }, `requestRaw ${options.method || "GET"} ${path14}`)
249
+ }, `requestRaw ${options.method || "GET"} ${path15}`)
249
250
  );
250
251
  }
251
- async request(path14, options = {}) {
252
- const resp = await this.requestRaw(path14, options);
252
+ async request(path15, options = {}) {
253
+ const resp = await this.requestRaw(path15, options);
253
254
  return resp.json();
254
255
  }
255
256
  async createIssue(title, description, labels) {
@@ -428,8 +429,8 @@ var GongfengClient = class {
428
429
  }
429
430
  return mr;
430
431
  }
431
- async requestGlobal(path14, options = {}) {
432
- const url = `${this.apiUrl}${path14}`;
432
+ async requestGlobal(path15, options = {}) {
433
+ const url = `${this.apiUrl}${path15}`;
433
434
  logger4.debug("API request (global)", { method: options.method || "GET", url });
434
435
  const resp = await this.circuitBreaker.execute(
435
436
  () => this.retryPolicy.execute(async () => {
@@ -446,7 +447,7 @@ var GongfengClient = class {
446
447
  throw new GongfengApiError(r.status, `Gongfeng API error ${r.status}: ${body}`, body);
447
448
  }
448
449
  return r;
449
- }, `requestGlobal ${options.method || "GET"} ${path14}`)
450
+ }, `requestGlobal ${options.method || "GET"} ${path15}`)
450
451
  );
451
452
  return resp.json();
452
453
  }
@@ -522,6 +523,7 @@ var IssueState = /* @__PURE__ */ ((IssueState2) => {
522
523
  IssueState2["PhaseWaiting"] = "phase_waiting";
523
524
  IssueState2["PhaseApproved"] = "phase_approved";
524
525
  IssueState2["ResolvingConflict"] = "resolving_conflict";
526
+ IssueState2["Paused"] = "paused";
525
527
  IssueState2["Completed"] = "completed";
526
528
  IssueState2["Failed"] = "failed";
527
529
  IssueState2["Deployed"] = "deployed";
@@ -550,6 +552,7 @@ var ActionLifecycleManager = class {
550
552
  this.addMapping("skipped" /* Skipped */, "init", "skipped");
551
553
  this.addMapping("branch_created" /* BranchCreated */, "init", "ready");
552
554
  this.addMapping("failed" /* Failed */, "init", "failed");
555
+ this.addMapping("paused" /* Paused */, "init", "paused");
553
556
  this.addMapping("deployed" /* Deployed */, "init", "done");
554
557
  this.addMapping("resolving_conflict" /* ResolvingConflict */, "conflict", "running");
555
558
  for (const spec of def.phases) {
@@ -593,6 +596,9 @@ var ActionLifecycleManager = class {
593
596
  * 对于通用状态 PhaseRunning/PhaseDone,需额外传入 currentPhase 来区分具体阶段。
594
597
  */
595
598
  resolve(state, currentPhase) {
599
+ if (state === "paused" /* Paused */ && currentPhase) {
600
+ return { action: currentPhase, status: "paused" };
601
+ }
596
602
  if (state === "phase_running" /* PhaseRunning */ && currentPhase) {
597
603
  return { action: currentPhase, status: "running" };
598
604
  }
@@ -648,6 +654,7 @@ var ActionLifecycleManager = class {
648
654
  * - done/skipped → 不可驱动
649
655
  */
650
656
  isDrivable(state, attempts, maxRetries, lastErrorRetryable) {
657
+ if (state === "paused" /* Paused */) return false;
651
658
  if (state === "phase_done" /* PhaseDone */) return true;
652
659
  if (state === "phase_approved" /* PhaseApproved */) return true;
653
660
  if (state === "phase_running" /* PhaseRunning */) return false;
@@ -664,6 +671,7 @@ var ActionLifecycleManager = class {
664
671
  case "running":
665
672
  case "done":
666
673
  case "skipped":
674
+ case "paused":
667
675
  return false;
668
676
  }
669
677
  }
@@ -772,6 +780,7 @@ var ActionLifecycleManager = class {
772
780
  }
773
781
  labels.set("completed" /* Completed */, t("state.completed"));
774
782
  labels.set("failed" /* Failed */, t("state.failed"));
783
+ labels.set("paused" /* Paused */, t("state.paused"));
775
784
  labels.set("resolving_conflict" /* ResolvingConflict */, t("state.resolvingConflict"));
776
785
  return labels;
777
786
  }
@@ -795,6 +804,19 @@ var ActionLifecycleManager = class {
795
804
  }
796
805
  return result;
797
806
  }
807
+ if (currentState === "paused" /* Paused */ && currentPhase) {
808
+ for (const phase of this.def.phases) {
809
+ if (passedCurrent) {
810
+ result[phase.name] = "pending";
811
+ } else if (phase.name === currentPhase) {
812
+ result[phase.name] = "in_progress";
813
+ passedCurrent = true;
814
+ } else {
815
+ result[phase.name] = "completed";
816
+ }
817
+ }
818
+ return result;
819
+ }
798
820
  if ((currentState === "phase_running" /* PhaseRunning */ || currentState === "phase_done" /* PhaseDone */) && currentPhase) {
799
821
  for (const phase of this.def.phases) {
800
822
  if (passedCurrent) {
@@ -1035,6 +1057,41 @@ var IssueTracker = class extends BaseTracker {
1035
1057
  logger5.warn("Issue marked as failed", { issueIid, error, failedAtState, attempts: record.attempts, isRetryable });
1036
1058
  eventBus.emitTyped("issue:failed", { issueIid, error, failedAtState, record });
1037
1059
  }
1060
+ pauseIssue(issueIid, currentPhase) {
1061
+ const record = this.collection[this.key(issueIid)];
1062
+ if (!record) return;
1063
+ record.state = "paused" /* Paused */;
1064
+ record.pausedAtPhase = currentPhase;
1065
+ record.failedAtState = void 0;
1066
+ record.lastError = void 0;
1067
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1068
+ this.save();
1069
+ logger5.info("Issue paused", { issueIid, pausedAtPhase: currentPhase });
1070
+ eventBus.emitTyped("issue:paused", { issueIid, pausedAtPhase: currentPhase, record });
1071
+ }
1072
+ resumeFromPause(issueIid, def, clearSession) {
1073
+ const record = this.collection[this.key(issueIid)];
1074
+ if (!record || record.state !== "paused" /* Paused */ || !record.pausedAtPhase) return false;
1075
+ const lm = new ActionLifecycleManager(def);
1076
+ const preState = lm.getPhasePreState(record.pausedAtPhase);
1077
+ if (!preState) return false;
1078
+ const phase = record.pausedAtPhase;
1079
+ record.state = preState;
1080
+ if (preState === "phase_done" /* PhaseDone */ || preState === "phase_approved" /* PhaseApproved */) {
1081
+ const phases = def.phases;
1082
+ const idx = phases.findIndex((p) => p.name === phase);
1083
+ if (idx > 0) {
1084
+ record.currentPhase = phases[idx - 1].name;
1085
+ }
1086
+ }
1087
+ record.pausedAtPhase = void 0;
1088
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1089
+ this.save();
1090
+ const eventType = clearSession ? "issue:redone" : "issue:continued";
1091
+ logger5.info("Issue resumed from pause", { issueIid, phase, clearSession, state: preState });
1092
+ eventBus.emitTyped(eventType, { issueIid, phase, record });
1093
+ return true;
1094
+ }
1038
1095
  isProcessing(issueIid) {
1039
1096
  const record = this.get(issueIid);
1040
1097
  if (!record) return false;
@@ -1158,6 +1215,7 @@ var IssueTracker = class extends BaseTracker {
1158
1215
  record.failedAtState = record.state;
1159
1216
  record.state = "failed" /* Failed */;
1160
1217
  record.lastError = "Interrupted by service restart";
1218
+ record.lastErrorRetryable = false;
1161
1219
  record.attempts += 1;
1162
1220
  record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1163
1221
  count++;
@@ -1167,6 +1225,16 @@ var IssueTracker = class extends BaseTracker {
1167
1225
  failedAtState: record.failedAtState,
1168
1226
  record
1169
1227
  });
1228
+ } else if (record.state === "failed" /* Failed */ && record.lastErrorRetryable !== false) {
1229
+ const iid = getIid(record);
1230
+ logger5.info("Marking pre-existing failed issue as non-retryable after restart", {
1231
+ issueIid: iid,
1232
+ currentPhase: record.currentPhase,
1233
+ attempts: record.attempts
1234
+ });
1235
+ record.lastErrorRetryable = false;
1236
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1237
+ count++;
1170
1238
  }
1171
1239
  }
1172
1240
  if (count > 0) {
@@ -2453,14 +2521,93 @@ var ReleasePhase = class extends BasePhase {
2453
2521
  };
2454
2522
 
2455
2523
  // src/phases/UatPhase.ts
2524
+ import fs7 from "fs";
2525
+ import path8 from "path";
2526
+ var logger9 = logger.child("UatPhase");
2456
2527
  function getDefaultHost() {
2457
2528
  return getLocalIP();
2458
2529
  }
2459
2530
  var UatPhase = class extends BasePhase {
2460
2531
  phaseName = "uat";
2532
+ currentCtx;
2461
2533
  getResultFiles(_ctx) {
2462
2534
  return [{ filename: "03-uat-report.md", label: "UAT \u62A5\u544A" }];
2463
2535
  }
2536
+ get planDir() {
2537
+ const displayId = Number(this.currentCtx?.demand.sourceRef.displayId ?? 0);
2538
+ return path8.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
2539
+ }
2540
+ /**
2541
+ * 检查产物是否已存在且内容足够。
2542
+ */
2543
+ hasArtifact(displayId) {
2544
+ const fp = path8.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`, "03-uat-report.md");
2545
+ if (!fs7.existsSync(fp)) return false;
2546
+ try {
2547
+ return fs7.statSync(fp).size >= BasePhase.MIN_ARTIFACT_BYTES;
2548
+ } catch {
2549
+ return false;
2550
+ }
2551
+ }
2552
+ async execute(ctx) {
2553
+ this.currentCtx = ctx;
2554
+ const displayId = Number(ctx.demand.sourceRef.displayId);
2555
+ if (this.hasArtifact(displayId)) {
2556
+ return this.completeWithExistingArtifact(ctx, displayId);
2557
+ }
2558
+ return this.launchAsyncUat(ctx, displayId);
2559
+ }
2560
+ /**
2561
+ * Mode 2 — 产物已就绪,验证并完成阶段。
2562
+ */
2563
+ async completeWithExistingArtifact(ctx, displayId) {
2564
+ logger9.info("UAT artifact found, completing phase", { iid: displayId });
2565
+ await this.notifyPhaseStart();
2566
+ await this.validatePhaseOutput(ctx, displayId);
2567
+ await this.notifyPhaseDone();
2568
+ this.plan.updatePhaseProgress(this.phaseName, "completed");
2569
+ await this.commitPlanFiles(ctx, displayId);
2570
+ await this.syncResultToIssue(ctx, displayId);
2571
+ return { success: true, output: "UAT report validated", exitCode: 0, gateRequested: false };
2572
+ }
2573
+ /**
2574
+ * Mode 1 — Fire-and-forget 启动 codebuddy,请求 gate 暂停。
2575
+ * codebuddy 的日志通过 onStreamEvent → EventBus → SSE 实时推送到前端。
2576
+ */
2577
+ async launchAsyncUat(ctx, displayId) {
2578
+ logger9.info("Launching async UAT", { iid: displayId });
2579
+ await this.notifyPhaseStart();
2580
+ this.plan.updatePhaseProgress(this.phaseName, "in_progress");
2581
+ await this.notifyComment(issueProgressComment(this.phaseName, "in_progress"));
2582
+ const prompt = this.buildPrompt(ctx);
2583
+ const runPromise = this.aiRunner.run({
2584
+ prompt,
2585
+ workDir: this.resolveAIWorkDir(ctx),
2586
+ timeoutMs: this.config.ai.phaseTimeoutMs,
2587
+ idleTimeoutMs: this.config.ai.idleTimeoutMs,
2588
+ onStreamEvent: (event) => {
2589
+ eventBus.emitTyped("agent:output", {
2590
+ issueIid: displayId,
2591
+ phase: this.phaseName,
2592
+ event
2593
+ });
2594
+ }
2595
+ });
2596
+ runPromise.then(async (result) => {
2597
+ if (result.success && this.hasArtifact(displayId)) {
2598
+ logger9.info("Async UAT completed successfully, approving gate", { iid: displayId });
2599
+ await this.hooks.onAsyncGateApproval?.(this.phaseName);
2600
+ } else {
2601
+ const reason = result.errorMessage || "UAT failed or artifact 03-uat-report.md missing";
2602
+ logger9.error("Async UAT failed", { iid: displayId, reason });
2603
+ await this.hooks.onPhaseFailed(this.phaseName, reason);
2604
+ }
2605
+ }).catch(async (err) => {
2606
+ logger9.error("Async UAT error", { iid: displayId, error: err.message });
2607
+ await this.hooks.onPhaseFailed(this.phaseName, err.message);
2608
+ });
2609
+ return { success: true, output: "UAT launched asynchronously", exitCode: 0, gateRequested: true };
2610
+ }
2464
2611
  buildPrompt(ctx) {
2465
2612
  const pc = demandToPromptContext(ctx.demand);
2466
2613
  const promptCtx = {
@@ -2594,6 +2741,7 @@ function buildPlanModePipeline(opts) {
2594
2741
  kind: "ai",
2595
2742
  startState: "phase_running" /* PhaseRunning */,
2596
2743
  doneState: "completed" /* Completed */,
2744
+ approvedState: "phase_approved" /* PhaseApproved */,
2597
2745
  retryable: true,
2598
2746
  artifacts: [
2599
2747
  { filename: "03-uat-report.md", label: "UAT\u62A5\u544A", editable: false }
@@ -2638,9 +2786,9 @@ function createLifecycleManager(def) {
2638
2786
 
2639
2787
  // src/workspace/WorkspaceConfig.ts
2640
2788
  import { z } from "zod";
2641
- import fs7 from "fs";
2642
- import path8 from "path";
2643
- var logger9 = logger.child("WorkspaceConfig");
2789
+ import fs8 from "fs";
2790
+ import path9 from "path";
2791
+ var logger10 = logger.child("WorkspaceConfig");
2644
2792
  var repoConfigSchema = z.object({
2645
2793
  name: z.string().min(1, "Repo name is required"),
2646
2794
  projectPath: z.string().min(1, "Gongfeng project path is required"),
@@ -2656,29 +2804,29 @@ var workspaceConfigSchema = z.object({
2656
2804
  });
2657
2805
  function loadWorkspaceConfig(configPath) {
2658
2806
  if (!configPath) return null;
2659
- if (!fs7.existsSync(configPath)) {
2660
- logger9.warn("Workspace config file not found, falling back to single-repo mode", {
2807
+ if (!fs8.existsSync(configPath)) {
2808
+ logger10.warn("Workspace config file not found, falling back to single-repo mode", {
2661
2809
  path: configPath
2662
2810
  });
2663
2811
  return null;
2664
2812
  }
2665
2813
  try {
2666
- const raw = fs7.readFileSync(configPath, "utf-8");
2814
+ const raw = fs8.readFileSync(configPath, "utf-8");
2667
2815
  const json = JSON.parse(raw);
2668
2816
  const result = workspaceConfigSchema.safeParse(json);
2669
2817
  if (!result.success) {
2670
2818
  const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
2671
- logger9.error(`Workspace config validation failed:
2819
+ logger10.error(`Workspace config validation failed:
2672
2820
  ${issues}`);
2673
2821
  return null;
2674
2822
  }
2675
- logger9.info("Workspace config loaded", {
2823
+ logger10.info("Workspace config loaded", {
2676
2824
  primary: result.data.primary.name,
2677
2825
  associates: result.data.associates.map((a) => a.name)
2678
2826
  });
2679
2827
  return result.data;
2680
2828
  } catch (err) {
2681
- logger9.error("Failed to parse workspace config", {
2829
+ logger10.error("Failed to parse workspace config", {
2682
2830
  path: configPath,
2683
2831
  error: err.message
2684
2832
  });
@@ -2704,14 +2852,14 @@ function isMultiRepo(ws) {
2704
2852
  }
2705
2853
  function persistWorkspaceConfig(ws, filePath) {
2706
2854
  try {
2707
- const dir = path8.dirname(filePath);
2708
- if (!fs7.existsSync(dir)) {
2709
- fs7.mkdirSync(dir, { recursive: true });
2855
+ const dir = path9.dirname(filePath);
2856
+ if (!fs8.existsSync(dir)) {
2857
+ fs8.mkdirSync(dir, { recursive: true });
2710
2858
  }
2711
- fs7.writeFileSync(filePath, JSON.stringify(ws, null, 2) + "\n", "utf-8");
2712
- logger9.info("Workspace config auto-generated from .env", { path: filePath });
2859
+ fs8.writeFileSync(filePath, JSON.stringify(ws, null, 2) + "\n", "utf-8");
2860
+ logger10.info("Workspace config auto-generated from .env", { path: filePath });
2713
2861
  } catch (err) {
2714
- logger9.warn("Failed to persist workspace config", {
2862
+ logger10.warn("Failed to persist workspace config", {
2715
2863
  path: filePath,
2716
2864
  error: err.message
2717
2865
  });
@@ -2719,12 +2867,12 @@ function persistWorkspaceConfig(ws, filePath) {
2719
2867
  }
2720
2868
 
2721
2869
  // src/workspace/WorkspaceManager.ts
2722
- import path9 from "path";
2723
- import fs8 from "fs/promises";
2870
+ import path10 from "path";
2871
+ import fs9 from "fs/promises";
2724
2872
  import { execFile } from "child_process";
2725
2873
  import { promisify } from "util";
2726
2874
  var execFileAsync = promisify(execFile);
2727
- var logger10 = logger.child("WorkspaceManager");
2875
+ var logger11 = logger.child("WorkspaceManager");
2728
2876
  var WorkspaceManager = class {
2729
2877
  wsConfig;
2730
2878
  worktreeBaseDir;
@@ -2753,7 +2901,7 @@ var WorkspaceManager = class {
2753
2901
  */
2754
2902
  async prepareWorkspace(issueIid, branchName, globalBaseBranch, globalBranchPrefix) {
2755
2903
  const wsRoot = this.getWorkspaceRoot(issueIid);
2756
- await fs8.mkdir(wsRoot, { recursive: true });
2904
+ await fs9.mkdir(wsRoot, { recursive: true });
2757
2905
  const primaryCtx = await this.preparePrimaryRepo(
2758
2906
  issueIid,
2759
2907
  branchName,
@@ -2772,7 +2920,7 @@ var WorkspaceManager = class {
2772
2920
  );
2773
2921
  associateCtxs.push(ctx);
2774
2922
  }
2775
- logger10.info("Workspace prepared", {
2923
+ logger11.info("Workspace prepared", {
2776
2924
  issueIid,
2777
2925
  wsRoot,
2778
2926
  repos: [primaryCtx.name, ...associateCtxs.map((a) => a.name)]
@@ -2797,7 +2945,7 @@ var WorkspaceManager = class {
2797
2945
  await git.commit(message);
2798
2946
  await git.push(wsCtx.branchName);
2799
2947
  committed.push(repo.name);
2800
- logger10.info("Committed and pushed changes", {
2948
+ logger11.info("Committed and pushed changes", {
2801
2949
  repo: repo.name,
2802
2950
  branch: wsCtx.branchName
2803
2951
  });
@@ -2811,19 +2959,19 @@ var WorkspaceManager = class {
2811
2959
  async cleanupWorkspace(wsCtx) {
2812
2960
  try {
2813
2961
  await this.mainGit.worktreeRemove(wsCtx.primary.gitRootDir, true);
2814
- logger10.info("Primary worktree removed", { dir: wsCtx.primary.gitRootDir });
2962
+ logger11.info("Primary worktree removed", { dir: wsCtx.primary.gitRootDir });
2815
2963
  } catch (err) {
2816
- logger10.warn("Failed to remove primary worktree", {
2964
+ logger11.warn("Failed to remove primary worktree", {
2817
2965
  dir: wsCtx.primary.gitRootDir,
2818
2966
  error: err.message
2819
2967
  });
2820
2968
  }
2821
2969
  for (const assoc of wsCtx.associates) {
2822
2970
  try {
2823
- await fs8.rm(assoc.gitRootDir, { recursive: true, force: true });
2824
- logger10.info("Associate repo dir removed", { name: assoc.name, dir: assoc.gitRootDir });
2971
+ await fs9.rm(assoc.gitRootDir, { recursive: true, force: true });
2972
+ logger11.info("Associate repo dir removed", { name: assoc.name, dir: assoc.gitRootDir });
2825
2973
  } catch (err) {
2826
- logger10.warn("Failed to remove associate repo dir", {
2974
+ logger11.warn("Failed to remove associate repo dir", {
2827
2975
  name: assoc.name,
2828
2976
  dir: assoc.gitRootDir,
2829
2977
  error: err.message
@@ -2831,9 +2979,9 @@ var WorkspaceManager = class {
2831
2979
  }
2832
2980
  }
2833
2981
  try {
2834
- const entries = await fs8.readdir(wsCtx.workspaceRoot);
2982
+ const entries = await fs9.readdir(wsCtx.workspaceRoot);
2835
2983
  if (entries.length === 0) {
2836
- await fs8.rmdir(wsCtx.workspaceRoot);
2984
+ await fs9.rmdir(wsCtx.workspaceRoot);
2837
2985
  }
2838
2986
  } catch {
2839
2987
  }
@@ -2845,13 +2993,13 @@ var WorkspaceManager = class {
2845
2993
  const wsRoot = this.getWorkspaceRoot(issueIid);
2846
2994
  const primary = this.wsConfig.primary;
2847
2995
  const defaultPrefix = globalBranchPrefix ?? primary.branchPrefix ?? "feat/issue";
2848
- const primaryDir = path9.join(wsRoot, primary.name);
2996
+ const primaryDir = path10.join(wsRoot, primary.name);
2849
2997
  const repos = [{
2850
2998
  name: primary.name,
2851
2999
  projectPath: primary.projectPath,
2852
3000
  role: primary.role ?? "",
2853
3001
  gitRootDir: primaryDir,
2854
- workDir: path9.join(primaryDir, primary.projectSubDir ?? ""),
3002
+ workDir: path10.join(primaryDir, primary.projectSubDir ?? ""),
2855
3003
  baseBranch: primary.baseBranch ?? globalBaseBranch,
2856
3004
  branchPrefix: primary.branchPrefix ?? defaultPrefix,
2857
3005
  isPrimary: true
@@ -2861,8 +3009,8 @@ var WorkspaceManager = class {
2861
3009
  name: assoc.name,
2862
3010
  projectPath: assoc.projectPath,
2863
3011
  role: assoc.role ?? "",
2864
- gitRootDir: path9.join(wsRoot, assoc.name),
2865
- workDir: path9.join(wsRoot, assoc.name, assoc.projectSubDir ?? ""),
3012
+ gitRootDir: path10.join(wsRoot, assoc.name),
3013
+ workDir: path10.join(wsRoot, assoc.name, assoc.projectSubDir ?? ""),
2866
3014
  baseBranch: assoc.baseBranch ?? globalBaseBranch,
2867
3015
  branchPrefix: assoc.branchPrefix ?? defaultPrefix,
2868
3016
  isPrimary: false
@@ -2871,12 +3019,12 @@ var WorkspaceManager = class {
2871
3019
  return repos;
2872
3020
  }
2873
3021
  getWorkspaceRoot(issueIid) {
2874
- return path9.join(this.worktreeBaseDir, `issue-${issueIid}`);
3022
+ return path10.join(this.worktreeBaseDir, `issue-${issueIid}`);
2875
3023
  }
2876
3024
  // ── Internal helpers ──
2877
3025
  async preparePrimaryRepo(issueIid, branchName, wsRoot, globalBaseBranch) {
2878
3026
  const primary = this.wsConfig.primary;
2879
- const repoDir = path9.join(wsRoot, primary.name);
3027
+ const repoDir = path10.join(wsRoot, primary.name);
2880
3028
  const baseBranch = primary.baseBranch ?? globalBaseBranch;
2881
3029
  await this.ensurePrimaryWorktree(repoDir, branchName, baseBranch);
2882
3030
  return {
@@ -2884,18 +3032,18 @@ var WorkspaceManager = class {
2884
3032
  projectPath: primary.projectPath,
2885
3033
  role: primary.role ?? "",
2886
3034
  gitRootDir: repoDir,
2887
- workDir: path9.join(repoDir, primary.projectSubDir ?? ""),
3035
+ workDir: path10.join(repoDir, primary.projectSubDir ?? ""),
2888
3036
  baseBranch,
2889
3037
  branchPrefix: primary.branchPrefix ?? "feat/issue",
2890
3038
  isPrimary: true
2891
3039
  };
2892
3040
  }
2893
3041
  async ensurePrimaryWorktree(repoDir, branchName, baseBranch) {
2894
- const wsRoot = path9.dirname(repoDir);
3042
+ const wsRoot = path10.dirname(repoDir);
2895
3043
  if (wsRoot !== repoDir) {
2896
3044
  try {
2897
- await fs8.access(path9.join(wsRoot, ".git"));
2898
- logger10.info("Migrating legacy worktree to primary subdir", { from: wsRoot, to: repoDir });
3045
+ await fs9.access(path10.join(wsRoot, ".git"));
3046
+ logger11.info("Migrating legacy worktree to primary subdir", { from: wsRoot, to: repoDir });
2899
3047
  await this.mainGit.worktreeRemove(wsRoot, true);
2900
3048
  await this.mainGit.worktreePrune();
2901
3049
  await this.cleanStaleDir(wsRoot);
@@ -2905,11 +3053,11 @@ var WorkspaceManager = class {
2905
3053
  const worktrees = await this.mainGit.worktreeList();
2906
3054
  if (worktrees.includes(repoDir)) {
2907
3055
  try {
2908
- await fs8.access(path9.join(repoDir, ".git"));
2909
- logger10.info("Reusing existing primary worktree", { dir: repoDir });
3056
+ await fs9.access(path10.join(repoDir, ".git"));
3057
+ logger11.info("Reusing existing primary worktree", { dir: repoDir });
2910
3058
  return;
2911
3059
  } catch {
2912
- logger10.warn("Primary worktree registered but .git missing, recreating", { dir: repoDir });
3060
+ logger11.warn("Primary worktree registered but .git missing, recreating", { dir: repoDir });
2913
3061
  await this.mainGit.worktreeRemove(repoDir, true);
2914
3062
  await this.mainGit.worktreePrune();
2915
3063
  }
@@ -2928,19 +3076,19 @@ var WorkspaceManager = class {
2928
3076
  await this.mainGit.worktreeAdd(repoDir, branchName, `origin/${baseBranch}`);
2929
3077
  }
2930
3078
  async prepareAssociateRepo(assoc, _issueIid, branchName, wsRoot, globalBaseBranch, globalBranchPrefix) {
2931
- const repoDir = path9.join(wsRoot, assoc.name);
3079
+ const repoDir = path10.join(wsRoot, assoc.name);
2932
3080
  const baseBranch = assoc.baseBranch ?? globalBaseBranch;
2933
3081
  const cloneUrl = `${this.gongfengApiUrl}/${assoc.projectPath}.git`;
2934
- const gitDirExists = await this.dirExists(path9.join(repoDir, ".git"));
3082
+ const gitDirExists = await this.dirExists(path10.join(repoDir, ".git"));
2935
3083
  if (!gitDirExists) {
2936
3084
  await this.cleanStaleDir(repoDir);
2937
- logger10.info("Cloning associate repo", { name: assoc.name, url: cloneUrl });
3085
+ logger11.info("Cloning associate repo", { name: assoc.name, url: cloneUrl });
2938
3086
  await execFileAsync("git", ["clone", "--depth", "50", cloneUrl, repoDir], {
2939
3087
  timeout: 3e5,
2940
3088
  maxBuffer: 10 * 1024 * 1024
2941
3089
  });
2942
3090
  } else {
2943
- logger10.info("Reusing existing associate clone", { name: assoc.name, dir: repoDir });
3091
+ logger11.info("Reusing existing associate clone", { name: assoc.name, dir: repoDir });
2944
3092
  }
2945
3093
  const assocGit = new GitOperations(repoDir);
2946
3094
  await assocGit.fetch();
@@ -2963,7 +3111,7 @@ var WorkspaceManager = class {
2963
3111
  projectPath: assoc.projectPath,
2964
3112
  role: assoc.role ?? "",
2965
3113
  gitRootDir: repoDir,
2966
- workDir: path9.join(repoDir, assoc.projectSubDir ?? ""),
3114
+ workDir: path10.join(repoDir, assoc.projectSubDir ?? ""),
2967
3115
  baseBranch,
2968
3116
  branchPrefix: assoc.branchPrefix ?? globalBranchPrefix,
2969
3117
  isPrimary: false
@@ -2971,13 +3119,13 @@ var WorkspaceManager = class {
2971
3119
  }
2972
3120
  async cleanStaleDir(dir) {
2973
3121
  if (await this.dirExists(dir)) {
2974
- logger10.warn("Removing stale directory", { dir });
2975
- await fs8.rm(dir, { recursive: true, force: true });
3122
+ logger11.warn("Removing stale directory", { dir });
3123
+ await fs9.rm(dir, { recursive: true, force: true });
2976
3124
  }
2977
3125
  }
2978
3126
  async dirExists(dir) {
2979
3127
  try {
2980
- await fs8.access(dir);
3128
+ await fs9.access(dir);
2981
3129
  return true;
2982
3130
  } catch {
2983
3131
  return false;
@@ -2986,8 +3134,8 @@ var WorkspaceManager = class {
2986
3134
  };
2987
3135
 
2988
3136
  // src/orchestrator/PipelineOrchestrator.ts
2989
- import path13 from "path";
2990
- import fs12 from "fs/promises";
3137
+ import path14 from "path";
3138
+ import fs13 from "fs/promises";
2991
3139
  import fsSync from "fs";
2992
3140
  import { execFile as execFile2 } from "child_process";
2993
3141
  import { promisify as promisify2 } from "util";
@@ -3020,8 +3168,8 @@ function mapSupplement(s) {
3020
3168
  }
3021
3169
 
3022
3170
  // src/utils/MergeRequestHelper.ts
3023
- import fs9 from "fs";
3024
- import path10 from "path";
3171
+ import fs10 from "fs";
3172
+ import path11 from "path";
3025
3173
  var TAPD_PATTERNS = [
3026
3174
  /--story=(\d+)/i,
3027
3175
  /--bug=(\d+)/i,
@@ -3065,9 +3213,9 @@ function generateMRDescription(options) {
3065
3213
  ];
3066
3214
  const planSections = [];
3067
3215
  for (const { filename, label } of summaryFiles) {
3068
- const filePath = path10.join(planDir, ".claude-plan", `issue-${issueIid}`, filename);
3069
- if (fs9.existsSync(filePath)) {
3070
- const content = fs9.readFileSync(filePath, "utf-8");
3216
+ const filePath = path11.join(planDir, ".claude-plan", `issue-${issueIid}`, filename);
3217
+ if (fs10.existsSync(filePath)) {
3218
+ const content = fs10.readFileSync(filePath, "utf-8");
3071
3219
  const summary = extractSummary(content);
3072
3220
  if (summary) {
3073
3221
  planSections.push(`### ${label}
@@ -3091,7 +3239,7 @@ function extractSummary(content, maxLines = 20) {
3091
3239
 
3092
3240
  // src/deploy/PortAllocator.ts
3093
3241
  import net from "net";
3094
- var logger11 = logger.child("PortAllocator");
3242
+ var logger12 = logger.child("PortAllocator");
3095
3243
  var DEFAULT_OPTIONS = {
3096
3244
  backendPortBase: 4e3,
3097
3245
  frontendPortBase: 9e3,
@@ -3116,7 +3264,7 @@ var PortAllocator = class {
3116
3264
  async allocate(issueIid) {
3117
3265
  const existing = this.allocated.get(issueIid);
3118
3266
  if (existing) {
3119
- logger11.info("Returning already allocated ports", { issueIid, ports: existing });
3267
+ logger12.info("Returning already allocated ports", { issueIid, ports: existing });
3120
3268
  return existing;
3121
3269
  }
3122
3270
  const usedBackend = new Set([...this.allocated.values()].map((p) => p.backendPort));
@@ -3134,10 +3282,10 @@ var PortAllocator = class {
3134
3282
  if (beOk && feOk) {
3135
3283
  const pair = { backendPort, frontendPort };
3136
3284
  this.allocated.set(issueIid, pair);
3137
- logger11.info("Ports allocated", { issueIid, ...pair });
3285
+ logger12.info("Ports allocated", { issueIid, ...pair });
3138
3286
  return pair;
3139
3287
  }
3140
- logger11.debug("Port pair unavailable, trying next", {
3288
+ logger12.debug("Port pair unavailable, trying next", {
3141
3289
  backendPort,
3142
3290
  frontendPort,
3143
3291
  beOk,
@@ -3152,7 +3300,7 @@ var PortAllocator = class {
3152
3300
  const pair = this.allocated.get(issueIid);
3153
3301
  if (pair) {
3154
3302
  this.allocated.delete(issueIid);
3155
- logger11.info("Ports released", { issueIid, ...pair });
3303
+ logger12.info("Ports released", { issueIid, ...pair });
3156
3304
  }
3157
3305
  }
3158
3306
  getPortsForIssue(issueIid) {
@@ -3163,15 +3311,15 @@ var PortAllocator = class {
3163
3311
  }
3164
3312
  restore(issueIid, ports) {
3165
3313
  this.allocated.set(issueIid, ports);
3166
- logger11.info("Ports restored from persistence", { issueIid, ...ports });
3314
+ logger12.info("Ports restored from persistence", { issueIid, ...ports });
3167
3315
  }
3168
3316
  };
3169
3317
 
3170
3318
  // src/deploy/DevServerManager.ts
3171
3319
  import { spawn } from "child_process";
3172
- import fs10 from "fs";
3173
- import path11 from "path";
3174
- var logger12 = logger.child("DevServerManager");
3320
+ import fs11 from "fs";
3321
+ import path12 from "path";
3322
+ var logger13 = logger.child("DevServerManager");
3175
3323
  var DEFAULT_OPTIONS2 = {};
3176
3324
  var DevServerManager = class {
3177
3325
  servers = /* @__PURE__ */ new Map();
@@ -3179,25 +3327,25 @@ var DevServerManager = class {
3179
3327
  logDir;
3180
3328
  constructor(options) {
3181
3329
  this.options = { ...DEFAULT_OPTIONS2, ...options };
3182
- this.logDir = path11.join(resolveDataDir(), "preview-logs");
3183
- if (!fs10.existsSync(this.logDir)) {
3184
- fs10.mkdirSync(this.logDir, { recursive: true });
3330
+ this.logDir = path12.join(resolveDataDir(), "preview-logs");
3331
+ if (!fs11.existsSync(this.logDir)) {
3332
+ fs11.mkdirSync(this.logDir, { recursive: true });
3185
3333
  }
3186
3334
  }
3187
3335
  getLogPath(issueIid, type) {
3188
- const filePath = path11.join(this.logDir, `${issueIid}-${type}.log`);
3189
- return fs10.existsSync(filePath) ? filePath : null;
3336
+ const filePath = path12.join(this.logDir, `${issueIid}-${type}.log`);
3337
+ return fs11.existsSync(filePath) ? filePath : null;
3190
3338
  }
3191
3339
  async startServers(wtCtx, ports) {
3192
3340
  if (this.servers.has(wtCtx.issueIid)) {
3193
- logger12.info("Servers already running for issue", { issueIid: wtCtx.issueIid });
3341
+ logger13.info("Servers already running for issue", { issueIid: wtCtx.issueIid });
3194
3342
  return;
3195
3343
  }
3196
- logger12.info("Starting dev servers", { issueIid: wtCtx.issueIid, ...ports });
3197
- const backendLogPath = path11.join(this.logDir, `${wtCtx.issueIid}-backend.log`);
3198
- const frontendLogPath = path11.join(this.logDir, `${wtCtx.issueIid}-frontend.log`);
3199
- const backendLog = fs10.createWriteStream(backendLogPath, { flags: "a" });
3200
- const frontendLog = fs10.createWriteStream(frontendLogPath, { flags: "a" });
3344
+ logger13.info("Starting dev servers", { issueIid: wtCtx.issueIid, ...ports });
3345
+ const backendLogPath = path12.join(this.logDir, `${wtCtx.issueIid}-backend.log`);
3346
+ const frontendLogPath = path12.join(this.logDir, `${wtCtx.issueIid}-frontend.log`);
3347
+ const backendLog = fs11.createWriteStream(backendLogPath, { flags: "a" });
3348
+ const frontendLog = fs11.createWriteStream(frontendLogPath, { flags: "a" });
3201
3349
  const tsLine = (stream, data) => `[${(/* @__PURE__ */ new Date()).toISOString()}] [${stream}] ${data.toString().trimEnd()}
3202
3350
  `;
3203
3351
  const backendEnv = {
@@ -3221,9 +3369,9 @@ var DevServerManager = class {
3221
3369
  backendLog.write(tsLine("stderr", data));
3222
3370
  });
3223
3371
  backend.on("exit", (code) => {
3224
- logger12.info("Backend process exited", { issueIid: wtCtx.issueIid, code });
3372
+ logger13.info("Backend process exited", { issueIid: wtCtx.issueIid, code });
3225
3373
  });
3226
- const frontendDir = path11.join(wtCtx.workDir, "frontend");
3374
+ const frontendDir = path12.join(wtCtx.workDir, "frontend");
3227
3375
  const frontendEnv = {
3228
3376
  ...process.env,
3229
3377
  BACKEND_PORT: String(ports.backendPort),
@@ -3245,7 +3393,7 @@ var DevServerManager = class {
3245
3393
  frontendLog.write(tsLine("stderr", data));
3246
3394
  });
3247
3395
  frontend.on("exit", (code) => {
3248
- logger12.info("Frontend process exited", { issueIid: wtCtx.issueIid, code });
3396
+ logger13.info("Frontend process exited", { issueIid: wtCtx.issueIid, code });
3249
3397
  });
3250
3398
  const serverSet = {
3251
3399
  backend,
@@ -3257,14 +3405,14 @@ var DevServerManager = class {
3257
3405
  frontendLog
3258
3406
  };
3259
3407
  this.servers.set(wtCtx.issueIid, serverSet);
3260
- logger12.info("Dev servers spawned, waiting for startup", { issueIid: wtCtx.issueIid, ...ports });
3408
+ logger13.info("Dev servers spawned, waiting for startup", { issueIid: wtCtx.issueIid, ...ports });
3261
3409
  await new Promise((r) => setTimeout(r, 1e4));
3262
- logger12.info("Dev servers startup grace period done", { issueIid: wtCtx.issueIid });
3410
+ logger13.info("Dev servers startup grace period done", { issueIid: wtCtx.issueIid });
3263
3411
  }
3264
3412
  stopServers(issueIid) {
3265
3413
  const set = this.servers.get(issueIid);
3266
3414
  if (!set) return;
3267
- logger12.info("Stopping dev servers", { issueIid, ports: set.ports });
3415
+ logger13.info("Stopping dev servers", { issueIid, ports: set.ports });
3268
3416
  killProcess(set.backend, `backend #${issueIid}`);
3269
3417
  killProcess(set.frontend, `frontend #${issueIid}`);
3270
3418
  set.backendLog.end();
@@ -3301,7 +3449,7 @@ function killProcess(proc, label) {
3301
3449
  }
3302
3450
  setTimeout(() => {
3303
3451
  if (!proc.killed && proc.exitCode === null) {
3304
- logger12.warn(`Force killing ${label}`);
3452
+ logger13.warn(`Force killing ${label}`);
3305
3453
  try {
3306
3454
  process.kill(-pid, "SIGKILL");
3307
3455
  } catch {
@@ -3310,7 +3458,7 @@ function killProcess(proc, label) {
3310
3458
  }
3311
3459
  }, 5e3);
3312
3460
  } catch (err) {
3313
- logger12.warn(`Failed to kill ${label}`, { error: err.message });
3461
+ logger13.warn(`Failed to kill ${label}`, { error: err.message });
3314
3462
  }
3315
3463
  }
3316
3464
 
@@ -3329,13 +3477,13 @@ function isE2eEnabledForIssue(issueIid, tracker, cfg) {
3329
3477
  }
3330
3478
 
3331
3479
  // src/e2e/ScreenshotCollector.ts
3332
- import fs11 from "fs";
3333
- import path12 from "path";
3334
- var logger13 = logger.child("ScreenshotCollector");
3480
+ import fs12 from "fs";
3481
+ import path13 from "path";
3482
+ var logger14 = logger.child("ScreenshotCollector");
3335
3483
  var MAX_SCREENSHOTS = 20;
3336
3484
  function walkDir(dir, files = []) {
3337
- for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
3338
- const full = path12.join(dir, entry.name);
3485
+ for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
3486
+ const full = path13.join(dir, entry.name);
3339
3487
  if (entry.isDirectory()) {
3340
3488
  walkDir(full, files);
3341
3489
  } else if (entry.isFile() && entry.name.endsWith(".png")) {
@@ -3345,34 +3493,34 @@ function walkDir(dir, files = []) {
3345
3493
  return files;
3346
3494
  }
3347
3495
  function collectScreenshots(workDir) {
3348
- const testResultsDir = path12.join(workDir, "frontend", "test-results");
3349
- if (!fs11.existsSync(testResultsDir)) {
3350
- logger13.debug("test-results directory not found", { dir: testResultsDir });
3496
+ const testResultsDir = path13.join(workDir, "frontend", "test-results");
3497
+ if (!fs12.existsSync(testResultsDir)) {
3498
+ logger14.debug("test-results directory not found", { dir: testResultsDir });
3351
3499
  return [];
3352
3500
  }
3353
3501
  const pngFiles = walkDir(testResultsDir);
3354
3502
  if (pngFiles.length === 0) {
3355
- logger13.debug("No screenshots found");
3503
+ logger14.debug("No screenshots found");
3356
3504
  return [];
3357
3505
  }
3358
3506
  const screenshots = pngFiles.map((filePath) => {
3359
- const relative = path12.relative(testResultsDir, filePath);
3360
- const testName = relative.split(path12.sep)[0] || path12.basename(filePath, ".png");
3507
+ const relative = path13.relative(testResultsDir, filePath);
3508
+ const testName = relative.split(path13.sep)[0] || path13.basename(filePath, ".png");
3361
3509
  return { filePath, testName };
3362
3510
  });
3363
3511
  if (screenshots.length > MAX_SCREENSHOTS) {
3364
- logger13.warn("Too many screenshots, truncating", {
3512
+ logger14.warn("Too many screenshots, truncating", {
3365
3513
  total: screenshots.length,
3366
3514
  max: MAX_SCREENSHOTS
3367
3515
  });
3368
3516
  return screenshots.slice(0, MAX_SCREENSHOTS);
3369
3517
  }
3370
- logger13.info("Screenshots collected", { count: screenshots.length });
3518
+ logger14.info("Screenshots collected", { count: screenshots.length });
3371
3519
  return screenshots;
3372
3520
  }
3373
3521
 
3374
3522
  // src/e2e/ScreenshotPublisher.ts
3375
- var logger14 = logger.child("ScreenshotPublisher");
3523
+ var logger15 = logger.child("ScreenshotPublisher");
3376
3524
  function buildComment(uploaded, truncated) {
3377
3525
  const lines = [t("screenshot.title"), ""];
3378
3526
  for (const item of uploaded) {
@@ -3391,12 +3539,12 @@ var ScreenshotPublisher = class {
3391
3539
  const { workDir, issueIid, issueId, mrIid } = options;
3392
3540
  const screenshots = collectScreenshots(workDir);
3393
3541
  if (screenshots.length === 0) {
3394
- logger14.info("No E2E screenshots to publish", { issueIid });
3542
+ logger15.info("No E2E screenshots to publish", { issueIid });
3395
3543
  return;
3396
3544
  }
3397
3545
  const uploaded = await this.uploadAll(screenshots);
3398
3546
  if (uploaded.length === 0) {
3399
- logger14.warn("All screenshot uploads failed", { issueIid });
3547
+ logger15.warn("All screenshot uploads failed", { issueIid });
3400
3548
  return;
3401
3549
  }
3402
3550
  const truncated = screenshots.length >= 20;
@@ -3405,7 +3553,7 @@ var ScreenshotPublisher = class {
3405
3553
  if (mrIid) {
3406
3554
  await this.postToMergeRequest(mrIid, comment);
3407
3555
  }
3408
- logger14.info("E2E screenshots published", {
3556
+ logger15.info("E2E screenshots published", {
3409
3557
  issueIid,
3410
3558
  mrIid,
3411
3559
  count: uploaded.length
@@ -3421,7 +3569,7 @@ var ScreenshotPublisher = class {
3421
3569
  markdown: result.markdown
3422
3570
  });
3423
3571
  } catch (err) {
3424
- logger14.warn("Failed to upload screenshot", {
3572
+ logger15.warn("Failed to upload screenshot", {
3425
3573
  filePath: screenshot.filePath,
3426
3574
  error: err.message
3427
3575
  });
@@ -3433,7 +3581,7 @@ var ScreenshotPublisher = class {
3433
3581
  try {
3434
3582
  await this.gongfeng.createIssueNote(issueId, comment);
3435
3583
  } catch (err) {
3436
- logger14.warn("Failed to post screenshots to issue", {
3584
+ logger15.warn("Failed to post screenshots to issue", {
3437
3585
  issueId,
3438
3586
  error: err.message
3439
3587
  });
@@ -3443,7 +3591,7 @@ var ScreenshotPublisher = class {
3443
3591
  try {
3444
3592
  await this.gongfeng.createMergeRequestNote(mrIid, comment);
3445
3593
  } catch (err) {
3446
- logger14.warn("Failed to post screenshots to merge request", {
3594
+ logger15.warn("Failed to post screenshots to merge request", {
3447
3595
  mrIid,
3448
3596
  error: err.message
3449
3597
  });
@@ -3626,7 +3774,7 @@ metrics.registerCounter("iaf_braindump_batches_total", "Total braindump batches"
3626
3774
  metrics.registerCounter("iaf_braindump_tasks_total", "Total braindump tasks");
3627
3775
 
3628
3776
  // src/orchestrator/steps/SetupStep.ts
3629
- var logger15 = logger.child("SetupStep");
3777
+ var logger16 = logger.child("SetupStep");
3630
3778
  async function executeSetup(ctx, deps) {
3631
3779
  const { issue, wtCtx, record, pipelineDef, branchName } = ctx;
3632
3780
  try {
@@ -3635,7 +3783,7 @@ async function executeSetup(ctx, deps) {
3635
3783
  "auto-finish:processing"
3636
3784
  ]);
3637
3785
  } catch (err) {
3638
- logger15.warn("Failed to update issue labels", { error: err.message });
3786
+ logger16.warn("Failed to update issue labels", { error: err.message });
3639
3787
  }
3640
3788
  await deps.mainGitMutex.runExclusive(async () => {
3641
3789
  deps.emitProgress(issue.iid, "fetch", t("orchestrator.fetchProgress"));
@@ -3687,7 +3835,7 @@ async function executeSetup(ctx, deps) {
3687
3835
  }
3688
3836
 
3689
3837
  // src/orchestrator/steps/PhaseLoopStep.ts
3690
- var logger16 = logger.child("PhaseLoopStep");
3838
+ var logger17 = logger.child("PhaseLoopStep");
3691
3839
  function resolveVerifyRunner(deps) {
3692
3840
  return deps.aiRunner;
3693
3841
  }
@@ -3717,13 +3865,17 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3717
3865
  const skippedDeployPhase = pipelineDef.phases.slice(0, startIdx).some((p) => p.deploysPreview);
3718
3866
  if (skippedDeployPhase && !phaseCtx.ports) {
3719
3867
  const existingPorts = deps.getPortsForIssue(issue.iid);
3720
- if (existingPorts) {
3721
- logger16.info("Restored preview ports from allocator", { iid: issue.iid, ...existingPorts });
3868
+ if (existingPorts && deps.isPreviewRunning(issue.iid)) {
3869
+ logger17.info("Restored preview ports from allocator", { iid: issue.iid, ...existingPorts });
3722
3870
  phaseCtx.ports = existingPorts;
3723
3871
  ctx.wtCtx.ports = existingPorts;
3724
3872
  serversStarted = true;
3725
3873
  } else {
3726
- logger16.info("Restarting preview servers for resumed pipeline", { iid: issue.iid });
3874
+ if (existingPorts) {
3875
+ logger17.info("Ports allocated but servers not running, restarting", { iid: issue.iid });
3876
+ } else {
3877
+ logger17.info("Restarting preview servers for resumed pipeline", { iid: issue.iid });
3878
+ }
3727
3879
  const ports = await deps.startPreviewServers(ctx.wtCtx, issue);
3728
3880
  if (ports) {
3729
3881
  phaseCtx.ports = ports;
@@ -3738,9 +3890,13 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3738
3890
  throw new ServiceShutdownError();
3739
3891
  }
3740
3892
  const spec = pipelineDef.phases[i];
3893
+ const pendingAction = deps.consumePendingAction?.(issue.iid);
3894
+ if (pendingAction) {
3895
+ throw new PhaseAbortedError(spec.name, pendingAction);
3896
+ }
3741
3897
  if (spec.kind === "gate") {
3742
3898
  if (deps.shouldAutoApprove(issue.labels)) {
3743
- logger16.info("Auto-approving review gate (matched autoApproveLabels)", {
3899
+ logger17.info("Auto-approving review gate (matched autoApproveLabels)", {
3744
3900
  iid: issue.iid,
3745
3901
  labels: issue.labels,
3746
3902
  autoApproveLabels: deps.config.review.autoApproveLabels
@@ -3761,7 +3917,7 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3761
3917
  deps.tracker.updateState(issue.iid, spec.startState, { currentPhase: spec.name });
3762
3918
  wtPlan.updatePhaseProgress(spec.name, "in_progress");
3763
3919
  deps.eventBus.emitTyped("review:requested", { issueIid: issue.iid });
3764
- logger16.info("Review gate reached, pausing", { iid: issue.iid });
3920
+ logger17.info("Review gate reached, pausing", { iid: issue.iid });
3765
3921
  return { serversStarted, paused: true };
3766
3922
  }
3767
3923
  if (spec.name === "verify" && deps.config.verifyFixLoop.enabled) {
@@ -3770,7 +3926,7 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3770
3926
  continue;
3771
3927
  }
3772
3928
  if (spec.name === "uat" && !isE2eEnabledForIssue(issue.iid, deps.tracker, deps.config)) {
3773
- logger16.info("UAT phase skipped (E2E not enabled for this issue)", { iid: issue.iid });
3929
+ logger17.info("UAT phase skipped (E2E not enabled for this issue)", { iid: issue.iid });
3774
3930
  deps.tracker.updateState(issue.iid, spec.doneState, { currentPhase: spec.name });
3775
3931
  wtPlan.updatePhaseProgress(spec.name, "completed");
3776
3932
  continue;
@@ -3779,7 +3935,7 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3779
3935
  const runner = spec.name === "verify" ? resolveVerifyRunner(deps) : spec.name === "uat" ? resolveUatRunner(deps, issue.iid) : deps.aiRunner;
3780
3936
  if (spec.name === "uat") {
3781
3937
  const runnerName = runner === deps.e2eAiRunner ? "e2eAiRunner (CodeBuddy)" : "mainRunner";
3782
- logger16.info("UAT phase starting", { iid: issue.iid, runner: runnerName });
3938
+ logger17.info("UAT phase starting", { iid: issue.iid, runner: runnerName });
3783
3939
  }
3784
3940
  const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config, lifecycleManager, hooks);
3785
3941
  if (wtGitMap && wtGitMap.size > 1) {
@@ -3789,8 +3945,9 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3789
3945
  if (spec.approvedState && result && "gateRequested" in result && result.gateRequested) {
3790
3946
  deps.tracker.updateState(issue.iid, "phase_waiting" /* PhaseWaiting */, { currentPhase: spec.name });
3791
3947
  wtPlan.updatePhaseProgress(spec.name, "gate_waiting");
3792
- deps.eventBus.emitTyped("release:gateRequested", { issueIid: issue.iid });
3793
- logger16.info("AI phase requested gate, pausing", { iid: issue.iid, phase: spec.name });
3948
+ const gateEvent = spec.name === "uat" ? "uat:gateRequested" : "release:gateRequested";
3949
+ deps.eventBus.emitTyped(gateEvent, { issueIid: issue.iid });
3950
+ logger17.info("AI phase requested gate, pausing", { iid: issue.iid, phase: spec.name });
3794
3951
  return { serversStarted, paused: true };
3795
3952
  }
3796
3953
  if (needsDeployment && !serversStarted && lifecycleManager.shouldDeployPreview(spec.name)) {
@@ -3817,7 +3974,7 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
3817
3974
  issueIid: issue.iid,
3818
3975
  maxIterations
3819
3976
  });
3820
- logger16.info("Verify-fix loop started", {
3977
+ logger17.info("Verify-fix loop started", {
3821
3978
  iid: issue.iid,
3822
3979
  maxIterations,
3823
3980
  buildPhaseIdx
@@ -3826,7 +3983,7 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
3826
3983
  if (isShuttingDown()) {
3827
3984
  throw new ServiceShutdownError();
3828
3985
  }
3829
- logger16.info("Verify-fix loop iteration", {
3986
+ logger17.info("Verify-fix loop iteration", {
3830
3987
  iteration,
3831
3988
  maxIterations,
3832
3989
  iid: issue.iid
@@ -3849,7 +4006,7 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
3849
4006
  try {
3850
4007
  verifyResult = await verifyPhase.execute(phaseCtx);
3851
4008
  } catch (err) {
3852
- logger16.warn("Verify phase execution failed", {
4009
+ logger17.warn("Verify phase execution failed", {
3853
4010
  iteration,
3854
4011
  iid: issue.iid,
3855
4012
  error: err.message
@@ -3881,13 +4038,13 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
3881
4038
  failures: report?.failureReasons
3882
4039
  });
3883
4040
  if (passed) {
3884
- logger16.info("Verify-fix loop passed", {
4041
+ logger17.info("Verify-fix loop passed", {
3885
4042
  iteration,
3886
4043
  iid: issue.iid
3887
4044
  });
3888
4045
  return;
3889
4046
  }
3890
- logger16.info("Verify failed, issues found", {
4047
+ logger17.info("Verify failed, issues found", {
3891
4048
  iteration,
3892
4049
  iid: issue.iid,
3893
4050
  failures: report?.failureReasons,
@@ -3900,7 +4057,7 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
3900
4057
  failures: report?.failureReasons ?? []
3901
4058
  });
3902
4059
  const failMsg = `Verify-fix loop exhausted after ${maxIterations} iterations. Remaining issues: ${report?.failureReasons?.join("; ") ?? "unknown"}`;
3903
- logger16.warn(failMsg, { iid: issue.iid });
4060
+ logger17.warn(failMsg, { iid: issue.iid });
3904
4061
  throw new AIExecutionError("verify", failMsg, {
3905
4062
  output: report?.rawReport ?? "",
3906
4063
  exitCode: 0
@@ -3917,7 +4074,7 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
3917
4074
  }
3918
4075
  async function executeBuildFix(ctx, deps, wtGit, wtPlan, buildPhaseIdx, fixContext, wtGitMap) {
3919
4076
  const { issue, lifecycleManager, phaseCtx } = ctx;
3920
- logger16.info("Looping back to build for fix", {
4077
+ logger17.info("Looping back to build for fix", {
3921
4078
  iteration: fixContext.iteration,
3922
4079
  iid: issue.iid,
3923
4080
  failures: fixContext.verifyFailures
@@ -3944,7 +4101,7 @@ async function executeBuildFix(ctx, deps, wtGit, wtPlan, buildPhaseIdx, fixConte
3944
4101
  }
3945
4102
 
3946
4103
  // src/orchestrator/steps/CompletionStep.ts
3947
- var logger17 = logger.child("CompletionStep");
4104
+ var logger18 = logger.child("CompletionStep");
3948
4105
  async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
3949
4106
  const { issue, branchName, wtCtx } = ctx;
3950
4107
  deps.emitProgress(issue.iid, "create_mr", t("orchestrator.createMrProgress"));
@@ -3976,7 +4133,7 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
3976
4133
  mrIid: void 0
3977
4134
  });
3978
4135
  } catch (err) {
3979
- logger17.warn("Failed to publish E2E screenshots", {
4136
+ logger18.warn("Failed to publish E2E screenshots", {
3980
4137
  iid: issue.iid,
3981
4138
  error: err.message
3982
4139
  });
@@ -3996,19 +4153,19 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
3996
4153
  await deps.claimer.releaseClaim(issue.id, issue.iid, "completed");
3997
4154
  }
3998
4155
  if (phaseResult.serversStarted && deps.config.preview.keepAfterComplete) {
3999
- logger17.info("Preview servers kept running after completion", { iid: issue.iid });
4156
+ logger18.info("Preview servers kept running after completion", { iid: issue.iid });
4000
4157
  } else {
4001
4158
  deps.stopPreviewServers(issue.iid);
4002
4159
  await deps.mainGitMutex.runExclusive(async () => {
4003
4160
  if (wtCtx.workspace) {
4004
4161
  await deps.workspaceManager.cleanupWorkspace(wtCtx.workspace);
4005
- logger17.info("Workspace cleaned up", { dir: wtCtx.workspace.workspaceRoot });
4162
+ logger18.info("Workspace cleaned up", { dir: wtCtx.workspace.workspaceRoot });
4006
4163
  } else {
4007
4164
  try {
4008
4165
  await deps.mainGit.worktreeRemove(wtCtx.gitRootDir, true);
4009
- logger17.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
4166
+ logger18.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
4010
4167
  } catch (err) {
4011
- logger17.warn("Failed to cleanup worktree", {
4168
+ logger18.warn("Failed to cleanup worktree", {
4012
4169
  dir: wtCtx.gitRootDir,
4013
4170
  error: err.message
4014
4171
  });
@@ -4016,15 +4173,15 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
4016
4173
  }
4017
4174
  });
4018
4175
  }
4019
- logger17.info("Issue processing completed", { iid: issue.iid });
4176
+ logger18.info("Issue processing completed", { iid: issue.iid });
4020
4177
  }
4021
4178
 
4022
4179
  // src/orchestrator/steps/FailureHandler.ts
4023
- var logger18 = logger.child("FailureHandler");
4180
+ var logger19 = logger.child("FailureHandler");
4024
4181
  async function handleFailure(err, issue, wtCtx, deps) {
4025
4182
  const errorMsg = err.message;
4026
4183
  const isRetryable = err instanceof AIExecutionError ? err.isRetryable : true;
4027
- logger18.error("Issue processing failed", { iid: issue.iid, error: errorMsg, isRetryable });
4184
+ logger19.error("Issue processing failed", { iid: issue.iid, error: errorMsg, isRetryable });
4028
4185
  metrics.incCounter("iaf_issues_failed_total");
4029
4186
  const currentRecord = deps.tracker.get(issue.iid);
4030
4187
  const failedAtState = currentRecord?.state || "pending" /* Pending */;
@@ -4033,7 +4190,11 @@ async function handleFailure(err, issue, wtCtx, deps) {
4033
4190
  deps.tracker.markFailed(issue.iid, errorMsg.slice(0, 500), failedAtState, isRetryable);
4034
4191
  }
4035
4192
  if (wasReset) {
4036
- logger18.info("Issue was reset during processing, skipping failure marking", { iid: issue.iid });
4193
+ logger19.info("Issue was reset during processing, skipping failure marking", { iid: issue.iid });
4194
+ throw err;
4195
+ }
4196
+ if (failedAtState === "paused" /* Paused */) {
4197
+ logger19.info("Issue was paused during processing, skipping failure handling", { iid: issue.iid });
4037
4198
  throw err;
4038
4199
  }
4039
4200
  try {
@@ -4055,7 +4216,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
4055
4216
  try {
4056
4217
  await deps.claimer.releaseClaim(issue.id, issue.iid, "failed");
4057
4218
  } catch (releaseErr) {
4058
- logger18.warn("Failed to release lock on failure", {
4219
+ logger19.warn("Failed to release lock on failure", {
4059
4220
  iid: issue.iid,
4060
4221
  error: releaseErr.message
4061
4222
  });
@@ -4063,7 +4224,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
4063
4224
  }
4064
4225
  deps.stopPreviewServers(issue.iid);
4065
4226
  const preservedDirs = wtCtx.workspace ? [wtCtx.workspace.primary.gitRootDir, ...wtCtx.workspace.associates.map((a) => a.gitRootDir)] : [wtCtx.gitRootDir];
4066
- logger18.info("Worktree(s) preserved for debugging", {
4227
+ logger19.info("Worktree(s) preserved for debugging", {
4067
4228
  primary: wtCtx.gitRootDir,
4068
4229
  all: preservedDirs
4069
4230
  });
@@ -4072,7 +4233,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
4072
4233
 
4073
4234
  // src/orchestrator/PipelineOrchestrator.ts
4074
4235
  var execFileAsync2 = promisify2(execFile2);
4075
- var logger19 = logger.child("PipelineOrchestrator");
4236
+ var logger20 = logger.child("PipelineOrchestrator");
4076
4237
  var PipelineOrchestrator = class {
4077
4238
  config;
4078
4239
  gongfeng;
@@ -4093,6 +4254,7 @@ var PipelineOrchestrator = class {
4093
4254
  workspaceManager;
4094
4255
  tenantId;
4095
4256
  effectiveWorktreeBaseDir;
4257
+ pendingActions = /* @__PURE__ */ new Map();
4096
4258
  /** 暴露 AIRunner 给外部(如 CommandExecutor 取消进程时使用) */
4097
4259
  getAIRunner() {
4098
4260
  return this.aiRunner;
@@ -4101,7 +4263,7 @@ var PipelineOrchestrator = class {
4101
4263
  setAIRunner(runner) {
4102
4264
  this.aiRunner = runner;
4103
4265
  this.conflictResolver = new ConflictResolver(runner);
4104
- logger19.info("AIRunner replaced via hot-reload");
4266
+ logger20.info("AIRunner replaced via hot-reload");
4105
4267
  }
4106
4268
  constructor(config, gongfeng, git, aiRunner, tracker, supplementStore, mainGitMutex, eventBusInstance, wsConfig, tenantId, e2eAiRunner) {
4107
4269
  this.config = config;
@@ -4119,14 +4281,14 @@ var PipelineOrchestrator = class {
4119
4281
  this.pipelineDef = mode === "plan-mode" ? buildPlanModePipeline({ releaseEnabled: config.release.enabled, e2eEnabled: config.e2e.enabled }) : getPipelineDef(mode);
4120
4282
  registerPipeline(this.pipelineDef);
4121
4283
  this.lifecycleManager = createLifecycleManager(this.pipelineDef);
4122
- logger19.info("Pipeline mode resolved", { tenantId: this.tenantId, mode: this.pipelineDef.mode, aiMode: config.ai.mode });
4284
+ logger20.info("Pipeline mode resolved", { tenantId: this.tenantId, mode: this.pipelineDef.mode, aiMode: config.ai.mode });
4123
4285
  this.portAllocator = new PortAllocator({
4124
4286
  backendPortBase: config.e2e.backendPortBase,
4125
4287
  frontendPortBase: config.e2e.frontendPortBase
4126
4288
  });
4127
4289
  this.devServerManager = new DevServerManager();
4128
4290
  this.screenshotPublisher = new ScreenshotPublisher(gongfeng);
4129
- this.effectiveWorktreeBaseDir = this.tenantId === "default" ? config.project.worktreeBaseDir : path13.join(config.project.worktreeBaseDir, this.tenantId);
4291
+ this.effectiveWorktreeBaseDir = this.tenantId === "default" ? config.project.worktreeBaseDir : path14.join(config.project.worktreeBaseDir, this.tenantId);
4130
4292
  const effectiveWsConfig = wsConfig ?? buildSingleRepoWorkspace(config.project, config.gongfeng.projectPath);
4131
4293
  this.workspaceManager = new WorkspaceManager({
4132
4294
  wsConfig: effectiveWsConfig,
@@ -4135,7 +4297,7 @@ var PipelineOrchestrator = class {
4135
4297
  mainGitMutex: this.mainGitMutex,
4136
4298
  gongfengApiUrl: config.gongfeng.apiUrl
4137
4299
  });
4138
- logger19.info("WorkspaceManager initialized", {
4300
+ logger20.info("WorkspaceManager initialized", {
4139
4301
  tenantId: this.tenantId,
4140
4302
  primary: effectiveWsConfig.primary.name,
4141
4303
  associates: effectiveWsConfig.associates.map((a) => a.name)
@@ -4156,7 +4318,7 @@ var PipelineOrchestrator = class {
4156
4318
  this.claimer = claimer;
4157
4319
  }
4158
4320
  async cleanupStaleState() {
4159
- logger19.info("Cleaning up stale worktree state...");
4321
+ logger20.info("Cleaning up stale worktree state...");
4160
4322
  let cleaned = 0;
4161
4323
  const repoGitRoot = this.config.project.gitRootDir;
4162
4324
  try {
@@ -4165,11 +4327,11 @@ var PipelineOrchestrator = class {
4165
4327
  if (wtDir === repoGitRoot) continue;
4166
4328
  if (!wtDir.includes("/issue-")) continue;
4167
4329
  try {
4168
- const gitFile = path13.join(wtDir, ".git");
4330
+ const gitFile = path14.join(wtDir, ".git");
4169
4331
  try {
4170
- await fs12.access(gitFile);
4332
+ await fs13.access(gitFile);
4171
4333
  } catch {
4172
- logger19.warn("Worktree corrupted (.git missing), force removing", { dir: wtDir });
4334
+ logger20.warn("Worktree corrupted (.git missing), force removing", { dir: wtDir });
4173
4335
  await this.mainGit.worktreeRemove(wtDir, true).catch(() => {
4174
4336
  });
4175
4337
  await this.mainGit.worktreePrune();
@@ -4178,37 +4340,49 @@ var PipelineOrchestrator = class {
4178
4340
  }
4179
4341
  const wtGit = new GitOperations(wtDir);
4180
4342
  if (await wtGit.isRebaseInProgress()) {
4181
- logger19.warn("Aborting residual rebase in worktree", { dir: wtDir });
4343
+ logger20.warn("Aborting residual rebase in worktree", { dir: wtDir });
4182
4344
  await wtGit.rebaseAbort();
4183
4345
  cleaned++;
4184
4346
  }
4185
- const indexLock = path13.join(wtDir, ".git", "index.lock");
4347
+ const indexLock = path14.join(wtDir, ".git", "index.lock");
4186
4348
  try {
4187
- await fs12.unlink(indexLock);
4188
- logger19.warn("Removed stale index.lock", { path: indexLock });
4349
+ await fs13.unlink(indexLock);
4350
+ logger20.warn("Removed stale index.lock", { path: indexLock });
4189
4351
  cleaned++;
4190
4352
  } catch {
4191
4353
  }
4192
4354
  } catch (err) {
4193
- logger19.warn("Failed to clean worktree state", { dir: wtDir, error: err.message });
4355
+ logger20.warn("Failed to clean worktree state", { dir: wtDir, error: err.message });
4194
4356
  }
4195
4357
  }
4196
4358
  } catch (err) {
4197
- logger19.warn("Failed to list worktrees for cleanup", { error: err.message });
4359
+ logger20.warn("Failed to list worktrees for cleanup", { error: err.message });
4198
4360
  }
4199
- const mainIndexLock = path13.join(repoGitRoot, ".git", "index.lock");
4361
+ const mainIndexLock = path14.join(repoGitRoot, ".git", "index.lock");
4200
4362
  try {
4201
- await fs12.unlink(mainIndexLock);
4202
- logger19.warn("Removed stale main repo index.lock", { path: mainIndexLock });
4363
+ await fs13.unlink(mainIndexLock);
4364
+ logger20.warn("Removed stale main repo index.lock", { path: mainIndexLock });
4203
4365
  cleaned++;
4204
4366
  } catch {
4205
4367
  }
4206
- logger19.info("Stale state cleanup complete", { cleaned });
4368
+ logger20.info("Stale state cleanup complete", { cleaned });
4207
4369
  }
4370
+ /**
4371
+ * 重启后清理幽灵端口分配。
4372
+ *
4373
+ * DevServerManager 的进程句柄仅存于内存,重启后全部丢失。
4374
+ * 此时 tracker 中残留的 ports 字段指向不可控的孤儿进程,
4375
+ * 必须清理以避免前端误显示 preview 状态。
4376
+ */
4208
4377
  restorePortAllocations() {
4209
4378
  for (const record of this.tracker.getAll()) {
4210
4379
  if (record.ports) {
4211
- this.portAllocator.restore(getIid(record), record.ports);
4380
+ const iid = getIid(record);
4381
+ logger20.info("Clearing stale port allocation after restart", { iid, ports: record.ports });
4382
+ this.tracker.updateState(iid, record.state, {
4383
+ ports: void 0,
4384
+ previewStartedAt: void 0
4385
+ });
4212
4386
  }
4213
4387
  }
4214
4388
  }
@@ -4249,20 +4423,20 @@ var PipelineOrchestrator = class {
4249
4423
  }
4250
4424
  try {
4251
4425
  await this.mainGit.worktreeRemove(wtCtx.gitRootDir, true);
4252
- logger19.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
4426
+ logger20.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
4253
4427
  } catch (err) {
4254
- logger19.warn("Failed to cleanup worktree", { dir: wtCtx.gitRootDir, error: err.message });
4428
+ logger20.warn("Failed to cleanup worktree", { dir: wtCtx.gitRootDir, error: err.message });
4255
4429
  }
4256
4430
  }
4257
4431
  async installDependencies(workDir) {
4258
- logger19.info("Installing dependencies in worktree", { workDir });
4432
+ logger20.info("Installing dependencies in worktree", { workDir });
4259
4433
  const knowledge = getProjectKnowledge() ?? KNOWLEDGE_DEFAULTS;
4260
4434
  const pkgMgr = knowledge.toolchain.packageManager.toLowerCase();
4261
4435
  const isNodeProject = ["npm", "pnpm", "yarn", "bun"].some((m) => pkgMgr.includes(m));
4262
4436
  if (isNodeProject) {
4263
4437
  const ready = await this.ensureNodeModules(workDir);
4264
4438
  if (ready) {
4265
- logger19.info("node_modules ready \u2014 skipping install");
4439
+ logger20.info("node_modules ready \u2014 skipping install");
4266
4440
  return;
4267
4441
  }
4268
4442
  }
@@ -4275,10 +4449,10 @@ var PipelineOrchestrator = class {
4275
4449
  maxBuffer: 10 * 1024 * 1024,
4276
4450
  timeout: 3e5
4277
4451
  });
4278
- logger19.info("Dependencies installed");
4452
+ logger20.info("Dependencies installed");
4279
4453
  } catch (err) {
4280
4454
  if (fallbackCmd) {
4281
- logger19.warn(`${installCmd} failed, retrying with fallback command`, {
4455
+ logger20.warn(`${installCmd} failed, retrying with fallback command`, {
4282
4456
  error: err.message
4283
4457
  });
4284
4458
  const [fallbackBin, ...fallbackArgs] = fallbackCmd.split(/\s+/);
@@ -4288,45 +4462,45 @@ var PipelineOrchestrator = class {
4288
4462
  maxBuffer: 10 * 1024 * 1024,
4289
4463
  timeout: 3e5
4290
4464
  });
4291
- logger19.info("Dependencies installed (fallback)");
4465
+ logger20.info("Dependencies installed (fallback)");
4292
4466
  } catch (retryErr) {
4293
- logger19.warn("Fallback install also failed", {
4467
+ logger20.warn("Fallback install also failed", {
4294
4468
  error: retryErr.message
4295
4469
  });
4296
4470
  }
4297
4471
  } else {
4298
- logger19.warn("Install failed, no fallback configured", {
4472
+ logger20.warn("Install failed, no fallback configured", {
4299
4473
  error: err.message
4300
4474
  });
4301
4475
  }
4302
4476
  }
4303
4477
  }
4304
4478
  async ensureNodeModules(workDir) {
4305
- const targetBin = path13.join(workDir, "node_modules", ".bin");
4479
+ const targetBin = path14.join(workDir, "node_modules", ".bin");
4306
4480
  try {
4307
- await fs12.access(targetBin);
4308
- logger19.info("node_modules already complete (has .bin/)");
4481
+ await fs13.access(targetBin);
4482
+ logger20.info("node_modules already complete (has .bin/)");
4309
4483
  return true;
4310
4484
  } catch {
4311
4485
  }
4312
- const sourceNM = path13.join(this.config.project.workDir, "node_modules");
4313
- const targetNM = path13.join(workDir, "node_modules");
4486
+ const sourceNM = path14.join(this.config.project.workDir, "node_modules");
4487
+ const targetNM = path14.join(workDir, "node_modules");
4314
4488
  try {
4315
- await fs12.access(sourceNM);
4489
+ await fs13.access(sourceNM);
4316
4490
  } catch {
4317
- logger19.warn("Main repo node_modules not found, skipping seed", { sourceNM });
4491
+ logger20.warn("Main repo node_modules not found, skipping seed", { sourceNM });
4318
4492
  return false;
4319
4493
  }
4320
- logger19.info("Seeding node_modules from main repo via reflink copy", { sourceNM, targetNM });
4494
+ logger20.info("Seeding node_modules from main repo via reflink copy", { sourceNM, targetNM });
4321
4495
  try {
4322
4496
  await execFileAsync2("rm", ["-rf", targetNM], { timeout: 6e4 });
4323
4497
  await execFileAsync2("cp", ["-a", "--reflink=auto", sourceNM, targetNM], {
4324
4498
  timeout: 12e4
4325
4499
  });
4326
- logger19.info("node_modules seeded from main repo");
4500
+ logger20.info("node_modules seeded from main repo");
4327
4501
  return true;
4328
4502
  } catch (err) {
4329
- logger19.warn("Failed to seed node_modules from main repo", {
4503
+ logger20.warn("Failed to seed node_modules from main repo", {
4330
4504
  error: err.message
4331
4505
  });
4332
4506
  return false;
@@ -4336,14 +4510,14 @@ var PipelineOrchestrator = class {
4336
4510
  const record = this.tracker.get(issueIid);
4337
4511
  if (!record) throw new IssueNotFoundError(issueIid);
4338
4512
  const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
4339
- logger19.info("Restarting issue \u2014 cleaning context", { issueIid, branchName: record.branchName });
4513
+ logger20.info("Restarting issue \u2014 cleaning context", { issueIid, branchName: record.branchName });
4340
4514
  this.aiRunner.killByWorkDir(wtCtx.workDir);
4341
4515
  this.stopPreviewServers(issueIid);
4342
4516
  try {
4343
4517
  const deleted = await this.gongfeng.cleanupAgentNotes(getExternalId(record));
4344
- logger19.info("Agent notes cleaned up", { issueIid, deleted });
4518
+ logger20.info("Agent notes cleaned up", { issueIid, deleted });
4345
4519
  } catch (err) {
4346
- logger19.warn("Failed to cleanup agent notes", { issueIid, error: err.message });
4520
+ logger20.warn("Failed to cleanup agent notes", { issueIid, error: err.message });
4347
4521
  }
4348
4522
  await this.mainGitMutex.runExclusive(async () => {
4349
4523
  await this.cleanupWorktree(wtCtx);
@@ -4357,19 +4531,19 @@ var PipelineOrchestrator = class {
4357
4531
  }
4358
4532
  });
4359
4533
  this.tracker.resetFull(issueIid);
4360
- logger19.info("Issue restarted", { issueIid });
4534
+ logger20.info("Issue restarted", { issueIid });
4361
4535
  }
4362
4536
  async cancelIssue(issueIid) {
4363
4537
  const record = this.tracker.get(issueIid);
4364
4538
  if (!record) throw new IssueNotFoundError(issueIid);
4365
4539
  const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
4366
- logger19.info("Cancelling issue \u2014 cleaning all resources", { issueIid, branchName: record.branchName });
4540
+ logger20.info("Cancelling issue \u2014 cleaning all resources", { issueIid, branchName: record.branchName });
4367
4541
  this.aiRunner.killByWorkDir(wtCtx.workDir);
4368
4542
  this.stopPreviewServers(issueIid);
4369
4543
  try {
4370
4544
  await this.gongfeng.removeLabelsWithPrefix(getExternalId(record), "auto-finish");
4371
4545
  } catch (err) {
4372
- logger19.warn("Failed to remove labels on cancel", { issueIid, error: err.message });
4546
+ logger20.warn("Failed to remove labels on cancel", { issueIid, error: err.message });
4373
4547
  }
4374
4548
  await this.mainGitMutex.runExclusive(async () => {
4375
4549
  await this.cleanupWorktree(wtCtx);
@@ -4383,7 +4557,7 @@ var PipelineOrchestrator = class {
4383
4557
  }
4384
4558
  });
4385
4559
  this.tracker.updateState(issueIid, "skipped" /* Skipped */);
4386
- logger19.info("Issue cancelled", { issueIid });
4560
+ logger20.info("Issue cancelled", { issueIid });
4387
4561
  }
4388
4562
  retryFromPhase(issueIid, phase) {
4389
4563
  const record = this.tracker.get(issueIid);
@@ -4393,12 +4567,107 @@ var PipelineOrchestrator = class {
4393
4567
  if (!issueLM.isRetryable(phase)) {
4394
4568
  throw new InvalidPhaseError(phase);
4395
4569
  }
4396
- logger19.info("Retrying issue from phase", { issueIid, phase });
4570
+ logger20.info("Retrying issue from phase", { issueIid, phase });
4397
4571
  const ok = this.tracker.resetToPhase(issueIid, phase, issueDef);
4398
4572
  if (!ok) {
4399
4573
  throw new InvalidPhaseError(phase);
4400
4574
  }
4401
4575
  }
4576
+ // ── 阶段级中止/继续/重做 ──
4577
+ abortIssue(issueIid) {
4578
+ const record = this.tracker.get(issueIid);
4579
+ if (!record) throw new IssueNotFoundError(issueIid);
4580
+ const ABORTABLE = /* @__PURE__ */ new Set([
4581
+ "phase_running" /* PhaseRunning */,
4582
+ "phase_done" /* PhaseDone */,
4583
+ "phase_waiting" /* PhaseWaiting */,
4584
+ "phase_approved" /* PhaseApproved */
4585
+ ]);
4586
+ if (!ABORTABLE.has(record.state)) {
4587
+ throw new InvalidStateError(record.state, `Issue #${issueIid} not in abortable state`);
4588
+ }
4589
+ const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
4590
+ if (record.state === "phase_running" /* PhaseRunning */) {
4591
+ this.pendingActions.set(issueIid, "abort");
4592
+ this.aiRunner.killByWorkDir(wtCtx.workDir);
4593
+ this.e2eAiRunner?.killByWorkDir(wtCtx.workDir);
4594
+ } else {
4595
+ this.tracker.pauseIssue(issueIid, record.currentPhase ?? "");
4596
+ }
4597
+ logger20.info("Issue abort requested", { issueIid, state: record.state });
4598
+ }
4599
+ continueIssue(issueIid) {
4600
+ const record = this.tracker.get(issueIid);
4601
+ if (!record) throw new IssueNotFoundError(issueIid);
4602
+ if (record.state !== "paused" /* Paused */) {
4603
+ throw new InvalidStateError(record.state, `Issue #${issueIid} not in paused state`);
4604
+ }
4605
+ const issueDef = this.getIssueSpecificPipelineDef(record);
4606
+ this.tracker.resumeFromPause(issueIid, issueDef, false);
4607
+ logger20.info("Issue continued from pause", { issueIid });
4608
+ }
4609
+ redoPhase(issueIid) {
4610
+ const record = this.tracker.get(issueIid);
4611
+ if (!record) throw new IssueNotFoundError(issueIid);
4612
+ const REDOABLE = /* @__PURE__ */ new Set([
4613
+ "paused" /* Paused */,
4614
+ "phase_running" /* PhaseRunning */,
4615
+ "phase_done" /* PhaseDone */,
4616
+ "phase_waiting" /* PhaseWaiting */,
4617
+ "phase_approved" /* PhaseApproved */
4618
+ ]);
4619
+ if (!REDOABLE.has(record.state)) {
4620
+ throw new InvalidStateError(record.state, `Issue #${issueIid} not in redoable state`);
4621
+ }
4622
+ const issueDef = this.getIssueSpecificPipelineDef(record);
4623
+ const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
4624
+ if (record.state === "phase_running" /* PhaseRunning */) {
4625
+ this.pendingActions.set(issueIid, "redo");
4626
+ this.aiRunner.killByWorkDir(wtCtx.workDir);
4627
+ this.e2eAiRunner?.killByWorkDir(wtCtx.workDir);
4628
+ } else if (record.state === "paused" /* Paused */) {
4629
+ const phase = record.pausedAtPhase;
4630
+ if (phase) {
4631
+ const wtPlan = new PlanPersistence(wtCtx.workDir, issueIid);
4632
+ wtPlan.updatePhaseProgress(phase, "pending");
4633
+ }
4634
+ this.tracker.resumeFromPause(issueIid, issueDef, true);
4635
+ this.eventBus.emitTyped("issue:redone", { issueIid });
4636
+ } else {
4637
+ const phase = record.currentPhase;
4638
+ if (phase) {
4639
+ const wtPlan = new PlanPersistence(wtCtx.workDir, issueIid);
4640
+ wtPlan.updatePhaseProgress(phase, "pending");
4641
+ this.tracker.resetToPhase(issueIid, phase, issueDef);
4642
+ }
4643
+ this.eventBus.emitTyped("issue:redone", { issueIid });
4644
+ }
4645
+ logger20.info("Issue redo requested", { issueIid, state: record.state });
4646
+ }
4647
+ /**
4648
+ * 处理中止/重做的共享逻辑:
4649
+ * - abort: 暂停 Issue(保留 session)
4650
+ * - redo: 重置阶段(清除 session)
4651
+ *
4652
+ * 由 catch 块的两条路径(PhaseAbortedError / pendingActions)共用。
4653
+ */
4654
+ applyPendingAction(action, issueIid, wtCtx, pipelineDef) {
4655
+ const rec = this.tracker.get(issueIid);
4656
+ if (rec?.state === "failed" /* Failed */ && rec.attempts > 0) {
4657
+ rec.attempts -= 1;
4658
+ }
4659
+ if (action === "abort") {
4660
+ this.tracker.pauseIssue(issueIid, rec?.currentPhase ?? "");
4661
+ } else {
4662
+ const phase = rec?.currentPhase;
4663
+ if (phase) {
4664
+ const wtPlan = new PlanPersistence(wtCtx.workDir, issueIid);
4665
+ wtPlan.updatePhaseProgress(phase, "pending");
4666
+ this.tracker.resetToPhase(issueIid, phase, pipelineDef);
4667
+ }
4668
+ this.eventBus.emitTyped("issue:redone", { issueIid });
4669
+ }
4670
+ }
4402
4671
  getIssueSpecificPipelineDef(record) {
4403
4672
  if (record.pipelineMode) {
4404
4673
  return getPipelineDef(record.pipelineMode);
@@ -4435,13 +4704,19 @@ var PipelineOrchestrator = class {
4435
4704
  stopPreviewServers: (iid) => this.stopPreviewServers(iid),
4436
4705
  tryCreateMergeRequest: (issue, branch, workDir, previewUrl) => this.tryCreateMergeRequest(issue, branch, workDir, previewUrl),
4437
4706
  buildPreviewUrl: (iid) => this.buildPreviewUrl(iid),
4438
- getPortsForIssue: (iid) => this.portAllocator.getPortsForIssue(iid)
4707
+ getPortsForIssue: (iid) => this.portAllocator.getPortsForIssue(iid),
4708
+ isPreviewRunning: (iid) => this.devServerManager.getStatus(iid).running,
4709
+ consumePendingAction: (iid) => {
4710
+ const action = this.pendingActions.get(iid);
4711
+ if (action) this.pendingActions.delete(iid);
4712
+ return action;
4713
+ }
4439
4714
  };
4440
4715
  }
4441
4716
  async _processIssueImpl(issue) {
4442
4717
  const branchName = `${this.config.project.branchPrefix}-${issue.iid}`;
4443
4718
  const wtCtx = this.computeWorktreeContext(issue.iid, branchName);
4444
- logger19.info("Processing issue", {
4719
+ logger20.info("Processing issue", {
4445
4720
  iid: issue.iid,
4446
4721
  title: issue.title,
4447
4722
  branchName,
@@ -4508,6 +4783,16 @@ var PipelineOrchestrator = class {
4508
4783
  if (phaseResult.paused) return;
4509
4784
  await executeCompletion(ctx, deps, phaseResult, wtGitMap);
4510
4785
  } catch (err) {
4786
+ if (err instanceof PhaseAbortedError) {
4787
+ this.applyPendingAction(err.action, issue.iid, wtCtx, issuePipelineDef);
4788
+ return;
4789
+ }
4790
+ const pendingAction = this.pendingActions.get(issue.iid);
4791
+ if (pendingAction) {
4792
+ this.pendingActions.delete(issue.iid);
4793
+ this.applyPendingAction(pendingAction, issue.iid, wtCtx, issuePipelineDef);
4794
+ return;
4795
+ }
4511
4796
  await handleFailure(err, issue, wtCtx, deps);
4512
4797
  }
4513
4798
  }
@@ -4536,7 +4821,7 @@ var PipelineOrchestrator = class {
4536
4821
  title,
4537
4822
  description
4538
4823
  });
4539
- logger19.info("Merge request created successfully", {
4824
+ logger20.info("Merge request created successfully", {
4540
4825
  iid: issue.iid,
4541
4826
  mrIid: mr.iid,
4542
4827
  mrUrl: mr.web_url
@@ -4544,7 +4829,7 @@ var PipelineOrchestrator = class {
4544
4829
  return { url: mr.web_url, iid: mr.iid };
4545
4830
  } catch (err) {
4546
4831
  const errorMsg = err.message;
4547
- logger19.warn("Failed to create merge request, trying to find existing one", {
4832
+ logger20.warn("Failed to create merge request, trying to find existing one", {
4548
4833
  iid: issue.iid,
4549
4834
  error: errorMsg
4550
4835
  });
@@ -4561,7 +4846,7 @@ var PipelineOrchestrator = class {
4561
4846
  this.config.project.baseBranch
4562
4847
  );
4563
4848
  if (existing) {
4564
- logger19.info("Found existing merge request", {
4849
+ logger20.info("Found existing merge request", {
4565
4850
  iid: issueIid,
4566
4851
  mrIid: existing.iid,
4567
4852
  mrUrl: existing.web_url
@@ -4569,7 +4854,7 @@ var PipelineOrchestrator = class {
4569
4854
  return { url: existing.web_url, iid: existing.iid };
4570
4855
  }
4571
4856
  } catch (findErr) {
4572
- logger19.warn("Failed to find existing merge request", {
4857
+ logger20.warn("Failed to find existing merge request", {
4573
4858
  iid: issueIid,
4574
4859
  error: findErr.message
4575
4860
  });
@@ -4603,6 +4888,11 @@ var PipelineOrchestrator = class {
4603
4888
  } catch {
4604
4889
  }
4605
4890
  },
4891
+ onAsyncGateApproval: async (phase) => {
4892
+ this.tracker.updateState(iid, "phase_approved" /* PhaseApproved */, { currentPhase: phase });
4893
+ this.eventBus.emitTyped("uat:completed", { issueIid: iid });
4894
+ logger20.info("Async gate approved via process callback", { iid, phase });
4895
+ },
4606
4896
  isNoteSyncEnabled: () => isNoteSyncEnabledForIssue(iid, this.tracker, this.config),
4607
4897
  isE2eEnabled: () => isE2eEnabledForIssue(iid, this.tracker, this.config)
4608
4898
  };
@@ -4635,7 +4925,7 @@ var PipelineOrchestrator = class {
4635
4925
  });
4636
4926
  return ports;
4637
4927
  } catch (err) {
4638
- logger19.error("Failed to start preview servers", {
4928
+ logger20.error("Failed to start preview servers", {
4639
4929
  iid: issue.iid,
4640
4930
  error: err.message
4641
4931
  });
@@ -4670,7 +4960,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4670
4960
  await this.mainGitMutex.runExclusive(async () => {
4671
4961
  await this.cleanupWorktree(wtCtx);
4672
4962
  });
4673
- logger19.info("Preview stopped and worktree cleaned", { iid: issueIid });
4963
+ logger20.info("Preview stopped and worktree cleaned", { iid: issueIid });
4674
4964
  }
4675
4965
  async markDeployed(issueIid) {
4676
4966
  const record = this.tracker.get(issueIid);
@@ -4687,7 +4977,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4687
4977
  try {
4688
4978
  await this.gongfeng.closeIssue(externalId);
4689
4979
  } catch (err) {
4690
- logger19.warn("Failed to close issue on Gongfeng", { iid: issueIid, error: err.message });
4980
+ logger20.warn("Failed to close issue on Gongfeng", { iid: issueIid, error: err.message });
4691
4981
  }
4692
4982
  try {
4693
4983
  const issue = await this.gongfeng.getIssueDetail(externalId);
@@ -4695,10 +4985,10 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4695
4985
  labels.push("auto-finish:deployed");
4696
4986
  await this.gongfeng.updateIssueLabels(externalId, labels);
4697
4987
  } catch (err) {
4698
- logger19.warn("Failed to update labels", { iid: issueIid, error: err.message });
4988
+ logger20.warn("Failed to update labels", { iid: issueIid, error: err.message });
4699
4989
  }
4700
4990
  this.tracker.updateState(issueIid, "deployed" /* Deployed */);
4701
- logger19.info("Issue marked as deployed", { iid: issueIid });
4991
+ logger20.info("Issue marked as deployed", { iid: issueIid });
4702
4992
  }
4703
4993
  async restartPreview(issueIid) {
4704
4994
  const record = this.tracker.get(issueIid);
@@ -4725,7 +5015,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4725
5015
  throw err;
4726
5016
  }
4727
5017
  const url = this.buildPreviewUrl(issueIid);
4728
- logger19.info("Preview restarted", { iid: issueIid, url });
5018
+ logger20.info("Preview restarted", { iid: issueIid, url });
4729
5019
  return url;
4730
5020
  }
4731
5021
  getPreviewHost() {
@@ -4758,7 +5048,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4758
5048
  if (!record) throw new IssueNotFoundError(issueIid);
4759
5049
  const baseBranch = this.config.project.baseBranch;
4760
5050
  const branchName = record.branchName;
4761
- logger19.info("Starting conflict resolution", { issueIid, branchName, baseBranch });
5051
+ logger20.info("Starting conflict resolution", { issueIid, branchName, baseBranch });
4762
5052
  this.tracker.updateState(issueIid, "resolving_conflict" /* ResolvingConflict */);
4763
5053
  this.eventBus.emitTyped("conflict:started", { issueIid });
4764
5054
  try {
@@ -4791,7 +5081,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4791
5081
  });
4792
5082
  }
4793
5083
  });
4794
- logger19.info("Running verification after conflict resolution", { issueIid });
5084
+ logger20.info("Running verification after conflict resolution", { issueIid });
4795
5085
  const wtPlan = new PlanPersistence(wtCtx.workDir, issueIid);
4796
5086
  wtPlan.ensureDir();
4797
5087
  const conflictLM = createLifecycleManager(this.getIssueSpecificPipelineDef(record));
@@ -4824,10 +5114,10 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4824
5114
  } catch {
4825
5115
  }
4826
5116
  await this.commentOnMr(record.mrUrl, t("conflict.mrResolvedComment"));
4827
- logger19.info("Conflict resolution completed", { issueIid });
5117
+ logger20.info("Conflict resolution completed", { issueIid });
4828
5118
  } catch (err) {
4829
5119
  const errorMsg = err.message;
4830
- logger19.error("Conflict resolution failed", { issueIid, error: errorMsg });
5120
+ logger20.error("Conflict resolution failed", { issueIid, error: errorMsg });
4831
5121
  try {
4832
5122
  const wtGit = new GitOperations(wtCtx.gitRootDir);
4833
5123
  if (await wtGit.isRebaseInProgress()) {
@@ -4857,7 +5147,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4857
5147
  try {
4858
5148
  await this.gongfeng.createMergeRequestNote(mrIid, body);
4859
5149
  } catch (err) {
4860
- logger19.warn("Failed to comment on MR", { mrIid, error: err.message });
5150
+ logger20.warn("Failed to comment on MR", { mrIid, error: err.message });
4861
5151
  }
4862
5152
  }
4863
5153
  };
@@ -4933,7 +5223,7 @@ ${questions}
4933
5223
  }
4934
5224
 
4935
5225
  // src/services/BrainstormService.ts
4936
- var logger20 = logger.child("Brainstorm");
5226
+ var logger21 = logger.child("Brainstorm");
4937
5227
  function agentConfigToAIConfig(agentCfg, timeoutMs) {
4938
5228
  return {
4939
5229
  mode: agentCfg.mode,
@@ -4969,7 +5259,7 @@ var BrainstormService = class {
4969
5259
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
4970
5260
  };
4971
5261
  this.sessions.set(session.id, session);
4972
- logger20.info("Created brainstorm session", { sessionId: session.id });
5262
+ logger21.info("Created brainstorm session", { sessionId: session.id });
4973
5263
  return session;
4974
5264
  }
4975
5265
  getSession(id) {
@@ -4978,7 +5268,7 @@ var BrainstormService = class {
4978
5268
  async generate(sessionId, onEvent) {
4979
5269
  const session = this.requireSession(sessionId);
4980
5270
  session.status = "generating";
4981
- logger20.info("Generating SDD", { sessionId });
5271
+ logger21.info("Generating SDD", { sessionId });
4982
5272
  const prompt = buildGeneratePrompt(session.transcript);
4983
5273
  const result = await this.generatorRunner.run({
4984
5274
  prompt,
@@ -5004,7 +5294,7 @@ var BrainstormService = class {
5004
5294
  const session = this.requireSession(sessionId);
5005
5295
  const roundNum = session.rounds.length + 1;
5006
5296
  session.status = "reviewing";
5007
- logger20.info("Reviewing SDD", { sessionId, round: roundNum });
5297
+ logger21.info("Reviewing SDD", { sessionId, round: roundNum });
5008
5298
  onEvent?.({ type: "round:start", data: { round: roundNum, phase: "review" }, round: roundNum });
5009
5299
  const prompt = buildReviewPrompt(session.currentSdd, roundNum);
5010
5300
  const result = await this.reviewerRunner.run({
@@ -5037,7 +5327,7 @@ var BrainstormService = class {
5037
5327
  throw new Error("No review round to refine from");
5038
5328
  }
5039
5329
  session.status = "refining";
5040
- logger20.info("Refining SDD", { sessionId, round: currentRound.round });
5330
+ logger21.info("Refining SDD", { sessionId, round: currentRound.round });
5041
5331
  const prompt = buildRefinePrompt(currentRound.questions);
5042
5332
  const result = await this.generatorRunner.run({
5043
5333
  prompt,
@@ -5122,4 +5412,4 @@ export {
5122
5412
  PipelineOrchestrator,
5123
5413
  BrainstormService
5124
5414
  };
5125
- //# sourceMappingURL=chunk-7YCDMVIF.js.map
5415
+ //# sourceMappingURL=chunk-5UPYA6KH.js.map