@xdevops/issue-auto-finish 1.0.83 → 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-UDCMSDNT.js → chunk-5UPYA6KH.js} +528 -214
  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-LFN7NUFS.js → chunk-JINMYD56.js} +3 -3
  15. package/dist/{chunk-GV2ORWT3.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 +3 -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-KFRHCALK.js → restart-RNXGTDWZ.js} +5 -5
  52. package/dist/run.js +11 -11
  53. package/dist/{start-46GW453L.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-GV2ORWT3.js.map +0 -1
  69. package/dist/chunk-KWODU7HB.js.map +0 -1
  70. package/dist/chunk-UDCMSDNT.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-LFN7NUFS.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-KFRHCALK.js.map → restart-RNXGTDWZ.js.map} +0 -0
  86. /package/dist/{start-46GW453L.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
  }
@@ -3713,14 +3861,42 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3713
3861
  );
3714
3862
  const needsDeployment = deps.shouldDeployServers(issue.iid);
3715
3863
  let serversStarted = false;
3864
+ if (needsDeployment && startIdx > 0) {
3865
+ const skippedDeployPhase = pipelineDef.phases.slice(0, startIdx).some((p) => p.deploysPreview);
3866
+ if (skippedDeployPhase && !phaseCtx.ports) {
3867
+ const existingPorts = deps.getPortsForIssue(issue.iid);
3868
+ if (existingPorts && deps.isPreviewRunning(issue.iid)) {
3869
+ logger17.info("Restored preview ports from allocator", { iid: issue.iid, ...existingPorts });
3870
+ phaseCtx.ports = existingPorts;
3871
+ ctx.wtCtx.ports = existingPorts;
3872
+ serversStarted = true;
3873
+ } else {
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
+ }
3879
+ const ports = await deps.startPreviewServers(ctx.wtCtx, issue);
3880
+ if (ports) {
3881
+ phaseCtx.ports = ports;
3882
+ ctx.wtCtx.ports = ports;
3883
+ serversStarted = true;
3884
+ }
3885
+ }
3886
+ }
3887
+ }
3716
3888
  for (let i = startIdx; i < pipelineDef.phases.length; i++) {
3717
3889
  if (isShuttingDown()) {
3718
3890
  throw new ServiceShutdownError();
3719
3891
  }
3720
3892
  const spec = pipelineDef.phases[i];
3893
+ const pendingAction = deps.consumePendingAction?.(issue.iid);
3894
+ if (pendingAction) {
3895
+ throw new PhaseAbortedError(spec.name, pendingAction);
3896
+ }
3721
3897
  if (spec.kind === "gate") {
3722
3898
  if (deps.shouldAutoApprove(issue.labels)) {
3723
- logger16.info("Auto-approving review gate (matched autoApproveLabels)", {
3899
+ logger17.info("Auto-approving review gate (matched autoApproveLabels)", {
3724
3900
  iid: issue.iid,
3725
3901
  labels: issue.labels,
3726
3902
  autoApproveLabels: deps.config.review.autoApproveLabels
@@ -3741,7 +3917,7 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3741
3917
  deps.tracker.updateState(issue.iid, spec.startState, { currentPhase: spec.name });
3742
3918
  wtPlan.updatePhaseProgress(spec.name, "in_progress");
3743
3919
  deps.eventBus.emitTyped("review:requested", { issueIid: issue.iid });
3744
- logger16.info("Review gate reached, pausing", { iid: issue.iid });
3920
+ logger17.info("Review gate reached, pausing", { iid: issue.iid });
3745
3921
  return { serversStarted, paused: true };
3746
3922
  }
3747
3923
  if (spec.name === "verify" && deps.config.verifyFixLoop.enabled) {
@@ -3750,7 +3926,7 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3750
3926
  continue;
3751
3927
  }
3752
3928
  if (spec.name === "uat" && !isE2eEnabledForIssue(issue.iid, deps.tracker, deps.config)) {
3753
- 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 });
3754
3930
  deps.tracker.updateState(issue.iid, spec.doneState, { currentPhase: spec.name });
3755
3931
  wtPlan.updatePhaseProgress(spec.name, "completed");
3756
3932
  continue;
@@ -3759,7 +3935,7 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3759
3935
  const runner = spec.name === "verify" ? resolveVerifyRunner(deps) : spec.name === "uat" ? resolveUatRunner(deps, issue.iid) : deps.aiRunner;
3760
3936
  if (spec.name === "uat") {
3761
3937
  const runnerName = runner === deps.e2eAiRunner ? "e2eAiRunner (CodeBuddy)" : "mainRunner";
3762
- logger16.info("UAT phase starting", { iid: issue.iid, runner: runnerName });
3938
+ logger17.info("UAT phase starting", { iid: issue.iid, runner: runnerName });
3763
3939
  }
3764
3940
  const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config, lifecycleManager, hooks);
3765
3941
  if (wtGitMap && wtGitMap.size > 1) {
@@ -3769,8 +3945,9 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3769
3945
  if (spec.approvedState && result && "gateRequested" in result && result.gateRequested) {
3770
3946
  deps.tracker.updateState(issue.iid, "phase_waiting" /* PhaseWaiting */, { currentPhase: spec.name });
3771
3947
  wtPlan.updatePhaseProgress(spec.name, "gate_waiting");
3772
- deps.eventBus.emitTyped("release:gateRequested", { issueIid: issue.iid });
3773
- 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 });
3774
3951
  return { serversStarted, paused: true };
3775
3952
  }
3776
3953
  if (needsDeployment && !serversStarted && lifecycleManager.shouldDeployPreview(spec.name)) {
@@ -3797,7 +3974,7 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
3797
3974
  issueIid: issue.iid,
3798
3975
  maxIterations
3799
3976
  });
3800
- logger16.info("Verify-fix loop started", {
3977
+ logger17.info("Verify-fix loop started", {
3801
3978
  iid: issue.iid,
3802
3979
  maxIterations,
3803
3980
  buildPhaseIdx
@@ -3806,7 +3983,7 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
3806
3983
  if (isShuttingDown()) {
3807
3984
  throw new ServiceShutdownError();
3808
3985
  }
3809
- logger16.info("Verify-fix loop iteration", {
3986
+ logger17.info("Verify-fix loop iteration", {
3810
3987
  iteration,
3811
3988
  maxIterations,
3812
3989
  iid: issue.iid
@@ -3829,7 +4006,7 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
3829
4006
  try {
3830
4007
  verifyResult = await verifyPhase.execute(phaseCtx);
3831
4008
  } catch (err) {
3832
- logger16.warn("Verify phase execution failed", {
4009
+ logger17.warn("Verify phase execution failed", {
3833
4010
  iteration,
3834
4011
  iid: issue.iid,
3835
4012
  error: err.message
@@ -3861,13 +4038,13 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
3861
4038
  failures: report?.failureReasons
3862
4039
  });
3863
4040
  if (passed) {
3864
- logger16.info("Verify-fix loop passed", {
4041
+ logger17.info("Verify-fix loop passed", {
3865
4042
  iteration,
3866
4043
  iid: issue.iid
3867
4044
  });
3868
4045
  return;
3869
4046
  }
3870
- logger16.info("Verify failed, issues found", {
4047
+ logger17.info("Verify failed, issues found", {
3871
4048
  iteration,
3872
4049
  iid: issue.iid,
3873
4050
  failures: report?.failureReasons,
@@ -3880,7 +4057,7 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
3880
4057
  failures: report?.failureReasons ?? []
3881
4058
  });
3882
4059
  const failMsg = `Verify-fix loop exhausted after ${maxIterations} iterations. Remaining issues: ${report?.failureReasons?.join("; ") ?? "unknown"}`;
3883
- logger16.warn(failMsg, { iid: issue.iid });
4060
+ logger17.warn(failMsg, { iid: issue.iid });
3884
4061
  throw new AIExecutionError("verify", failMsg, {
3885
4062
  output: report?.rawReport ?? "",
3886
4063
  exitCode: 0
@@ -3897,7 +4074,7 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
3897
4074
  }
3898
4075
  async function executeBuildFix(ctx, deps, wtGit, wtPlan, buildPhaseIdx, fixContext, wtGitMap) {
3899
4076
  const { issue, lifecycleManager, phaseCtx } = ctx;
3900
- logger16.info("Looping back to build for fix", {
4077
+ logger17.info("Looping back to build for fix", {
3901
4078
  iteration: fixContext.iteration,
3902
4079
  iid: issue.iid,
3903
4080
  failures: fixContext.verifyFailures
@@ -3924,7 +4101,7 @@ async function executeBuildFix(ctx, deps, wtGit, wtPlan, buildPhaseIdx, fixConte
3924
4101
  }
3925
4102
 
3926
4103
  // src/orchestrator/steps/CompletionStep.ts
3927
- var logger17 = logger.child("CompletionStep");
4104
+ var logger18 = logger.child("CompletionStep");
3928
4105
  async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
3929
4106
  const { issue, branchName, wtCtx } = ctx;
3930
4107
  deps.emitProgress(issue.iid, "create_mr", t("orchestrator.createMrProgress"));
@@ -3956,7 +4133,7 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
3956
4133
  mrIid: void 0
3957
4134
  });
3958
4135
  } catch (err) {
3959
- logger17.warn("Failed to publish E2E screenshots", {
4136
+ logger18.warn("Failed to publish E2E screenshots", {
3960
4137
  iid: issue.iid,
3961
4138
  error: err.message
3962
4139
  });
@@ -3976,19 +4153,19 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
3976
4153
  await deps.claimer.releaseClaim(issue.id, issue.iid, "completed");
3977
4154
  }
3978
4155
  if (phaseResult.serversStarted && deps.config.preview.keepAfterComplete) {
3979
- logger17.info("Preview servers kept running after completion", { iid: issue.iid });
4156
+ logger18.info("Preview servers kept running after completion", { iid: issue.iid });
3980
4157
  } else {
3981
4158
  deps.stopPreviewServers(issue.iid);
3982
4159
  await deps.mainGitMutex.runExclusive(async () => {
3983
4160
  if (wtCtx.workspace) {
3984
4161
  await deps.workspaceManager.cleanupWorkspace(wtCtx.workspace);
3985
- logger17.info("Workspace cleaned up", { dir: wtCtx.workspace.workspaceRoot });
4162
+ logger18.info("Workspace cleaned up", { dir: wtCtx.workspace.workspaceRoot });
3986
4163
  } else {
3987
4164
  try {
3988
4165
  await deps.mainGit.worktreeRemove(wtCtx.gitRootDir, true);
3989
- logger17.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
4166
+ logger18.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
3990
4167
  } catch (err) {
3991
- logger17.warn("Failed to cleanup worktree", {
4168
+ logger18.warn("Failed to cleanup worktree", {
3992
4169
  dir: wtCtx.gitRootDir,
3993
4170
  error: err.message
3994
4171
  });
@@ -3996,15 +4173,15 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
3996
4173
  }
3997
4174
  });
3998
4175
  }
3999
- logger17.info("Issue processing completed", { iid: issue.iid });
4176
+ logger18.info("Issue processing completed", { iid: issue.iid });
4000
4177
  }
4001
4178
 
4002
4179
  // src/orchestrator/steps/FailureHandler.ts
4003
- var logger18 = logger.child("FailureHandler");
4180
+ var logger19 = logger.child("FailureHandler");
4004
4181
  async function handleFailure(err, issue, wtCtx, deps) {
4005
4182
  const errorMsg = err.message;
4006
4183
  const isRetryable = err instanceof AIExecutionError ? err.isRetryable : true;
4007
- logger18.error("Issue processing failed", { iid: issue.iid, error: errorMsg, isRetryable });
4184
+ logger19.error("Issue processing failed", { iid: issue.iid, error: errorMsg, isRetryable });
4008
4185
  metrics.incCounter("iaf_issues_failed_total");
4009
4186
  const currentRecord = deps.tracker.get(issue.iid);
4010
4187
  const failedAtState = currentRecord?.state || "pending" /* Pending */;
@@ -4013,7 +4190,11 @@ async function handleFailure(err, issue, wtCtx, deps) {
4013
4190
  deps.tracker.markFailed(issue.iid, errorMsg.slice(0, 500), failedAtState, isRetryable);
4014
4191
  }
4015
4192
  if (wasReset) {
4016
- 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 });
4017
4198
  throw err;
4018
4199
  }
4019
4200
  try {
@@ -4035,7 +4216,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
4035
4216
  try {
4036
4217
  await deps.claimer.releaseClaim(issue.id, issue.iid, "failed");
4037
4218
  } catch (releaseErr) {
4038
- logger18.warn("Failed to release lock on failure", {
4219
+ logger19.warn("Failed to release lock on failure", {
4039
4220
  iid: issue.iid,
4040
4221
  error: releaseErr.message
4041
4222
  });
@@ -4043,7 +4224,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
4043
4224
  }
4044
4225
  deps.stopPreviewServers(issue.iid);
4045
4226
  const preservedDirs = wtCtx.workspace ? [wtCtx.workspace.primary.gitRootDir, ...wtCtx.workspace.associates.map((a) => a.gitRootDir)] : [wtCtx.gitRootDir];
4046
- logger18.info("Worktree(s) preserved for debugging", {
4227
+ logger19.info("Worktree(s) preserved for debugging", {
4047
4228
  primary: wtCtx.gitRootDir,
4048
4229
  all: preservedDirs
4049
4230
  });
@@ -4052,7 +4233,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
4052
4233
 
4053
4234
  // src/orchestrator/PipelineOrchestrator.ts
4054
4235
  var execFileAsync2 = promisify2(execFile2);
4055
- var logger19 = logger.child("PipelineOrchestrator");
4236
+ var logger20 = logger.child("PipelineOrchestrator");
4056
4237
  var PipelineOrchestrator = class {
4057
4238
  config;
4058
4239
  gongfeng;
@@ -4073,6 +4254,7 @@ var PipelineOrchestrator = class {
4073
4254
  workspaceManager;
4074
4255
  tenantId;
4075
4256
  effectiveWorktreeBaseDir;
4257
+ pendingActions = /* @__PURE__ */ new Map();
4076
4258
  /** 暴露 AIRunner 给外部(如 CommandExecutor 取消进程时使用) */
4077
4259
  getAIRunner() {
4078
4260
  return this.aiRunner;
@@ -4081,7 +4263,7 @@ var PipelineOrchestrator = class {
4081
4263
  setAIRunner(runner) {
4082
4264
  this.aiRunner = runner;
4083
4265
  this.conflictResolver = new ConflictResolver(runner);
4084
- logger19.info("AIRunner replaced via hot-reload");
4266
+ logger20.info("AIRunner replaced via hot-reload");
4085
4267
  }
4086
4268
  constructor(config, gongfeng, git, aiRunner, tracker, supplementStore, mainGitMutex, eventBusInstance, wsConfig, tenantId, e2eAiRunner) {
4087
4269
  this.config = config;
@@ -4099,14 +4281,14 @@ var PipelineOrchestrator = class {
4099
4281
  this.pipelineDef = mode === "plan-mode" ? buildPlanModePipeline({ releaseEnabled: config.release.enabled, e2eEnabled: config.e2e.enabled }) : getPipelineDef(mode);
4100
4282
  registerPipeline(this.pipelineDef);
4101
4283
  this.lifecycleManager = createLifecycleManager(this.pipelineDef);
4102
- 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 });
4103
4285
  this.portAllocator = new PortAllocator({
4104
4286
  backendPortBase: config.e2e.backendPortBase,
4105
4287
  frontendPortBase: config.e2e.frontendPortBase
4106
4288
  });
4107
4289
  this.devServerManager = new DevServerManager();
4108
4290
  this.screenshotPublisher = new ScreenshotPublisher(gongfeng);
4109
- 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);
4110
4292
  const effectiveWsConfig = wsConfig ?? buildSingleRepoWorkspace(config.project, config.gongfeng.projectPath);
4111
4293
  this.workspaceManager = new WorkspaceManager({
4112
4294
  wsConfig: effectiveWsConfig,
@@ -4115,7 +4297,7 @@ var PipelineOrchestrator = class {
4115
4297
  mainGitMutex: this.mainGitMutex,
4116
4298
  gongfengApiUrl: config.gongfeng.apiUrl
4117
4299
  });
4118
- logger19.info("WorkspaceManager initialized", {
4300
+ logger20.info("WorkspaceManager initialized", {
4119
4301
  tenantId: this.tenantId,
4120
4302
  primary: effectiveWsConfig.primary.name,
4121
4303
  associates: effectiveWsConfig.associates.map((a) => a.name)
@@ -4136,7 +4318,7 @@ var PipelineOrchestrator = class {
4136
4318
  this.claimer = claimer;
4137
4319
  }
4138
4320
  async cleanupStaleState() {
4139
- logger19.info("Cleaning up stale worktree state...");
4321
+ logger20.info("Cleaning up stale worktree state...");
4140
4322
  let cleaned = 0;
4141
4323
  const repoGitRoot = this.config.project.gitRootDir;
4142
4324
  try {
@@ -4145,11 +4327,11 @@ var PipelineOrchestrator = class {
4145
4327
  if (wtDir === repoGitRoot) continue;
4146
4328
  if (!wtDir.includes("/issue-")) continue;
4147
4329
  try {
4148
- const gitFile = path13.join(wtDir, ".git");
4330
+ const gitFile = path14.join(wtDir, ".git");
4149
4331
  try {
4150
- await fs12.access(gitFile);
4332
+ await fs13.access(gitFile);
4151
4333
  } catch {
4152
- logger19.warn("Worktree corrupted (.git missing), force removing", { dir: wtDir });
4334
+ logger20.warn("Worktree corrupted (.git missing), force removing", { dir: wtDir });
4153
4335
  await this.mainGit.worktreeRemove(wtDir, true).catch(() => {
4154
4336
  });
4155
4337
  await this.mainGit.worktreePrune();
@@ -4158,37 +4340,49 @@ var PipelineOrchestrator = class {
4158
4340
  }
4159
4341
  const wtGit = new GitOperations(wtDir);
4160
4342
  if (await wtGit.isRebaseInProgress()) {
4161
- logger19.warn("Aborting residual rebase in worktree", { dir: wtDir });
4343
+ logger20.warn("Aborting residual rebase in worktree", { dir: wtDir });
4162
4344
  await wtGit.rebaseAbort();
4163
4345
  cleaned++;
4164
4346
  }
4165
- const indexLock = path13.join(wtDir, ".git", "index.lock");
4347
+ const indexLock = path14.join(wtDir, ".git", "index.lock");
4166
4348
  try {
4167
- await fs12.unlink(indexLock);
4168
- logger19.warn("Removed stale index.lock", { path: indexLock });
4349
+ await fs13.unlink(indexLock);
4350
+ logger20.warn("Removed stale index.lock", { path: indexLock });
4169
4351
  cleaned++;
4170
4352
  } catch {
4171
4353
  }
4172
4354
  } catch (err) {
4173
- 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 });
4174
4356
  }
4175
4357
  }
4176
4358
  } catch (err) {
4177
- logger19.warn("Failed to list worktrees for cleanup", { error: err.message });
4359
+ logger20.warn("Failed to list worktrees for cleanup", { error: err.message });
4178
4360
  }
4179
- const mainIndexLock = path13.join(repoGitRoot, ".git", "index.lock");
4361
+ const mainIndexLock = path14.join(repoGitRoot, ".git", "index.lock");
4180
4362
  try {
4181
- await fs12.unlink(mainIndexLock);
4182
- 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 });
4183
4365
  cleaned++;
4184
4366
  } catch {
4185
4367
  }
4186
- logger19.info("Stale state cleanup complete", { cleaned });
4368
+ logger20.info("Stale state cleanup complete", { cleaned });
4187
4369
  }
4370
+ /**
4371
+ * 重启后清理幽灵端口分配。
4372
+ *
4373
+ * DevServerManager 的进程句柄仅存于内存,重启后全部丢失。
4374
+ * 此时 tracker 中残留的 ports 字段指向不可控的孤儿进程,
4375
+ * 必须清理以避免前端误显示 preview 状态。
4376
+ */
4188
4377
  restorePortAllocations() {
4189
4378
  for (const record of this.tracker.getAll()) {
4190
4379
  if (record.ports) {
4191
- 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
+ });
4192
4386
  }
4193
4387
  }
4194
4388
  }
@@ -4229,20 +4423,20 @@ var PipelineOrchestrator = class {
4229
4423
  }
4230
4424
  try {
4231
4425
  await this.mainGit.worktreeRemove(wtCtx.gitRootDir, true);
4232
- logger19.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
4426
+ logger20.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
4233
4427
  } catch (err) {
4234
- 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 });
4235
4429
  }
4236
4430
  }
4237
4431
  async installDependencies(workDir) {
4238
- logger19.info("Installing dependencies in worktree", { workDir });
4432
+ logger20.info("Installing dependencies in worktree", { workDir });
4239
4433
  const knowledge = getProjectKnowledge() ?? KNOWLEDGE_DEFAULTS;
4240
4434
  const pkgMgr = knowledge.toolchain.packageManager.toLowerCase();
4241
4435
  const isNodeProject = ["npm", "pnpm", "yarn", "bun"].some((m) => pkgMgr.includes(m));
4242
4436
  if (isNodeProject) {
4243
4437
  const ready = await this.ensureNodeModules(workDir);
4244
4438
  if (ready) {
4245
- logger19.info("node_modules ready \u2014 skipping install");
4439
+ logger20.info("node_modules ready \u2014 skipping install");
4246
4440
  return;
4247
4441
  }
4248
4442
  }
@@ -4255,10 +4449,10 @@ var PipelineOrchestrator = class {
4255
4449
  maxBuffer: 10 * 1024 * 1024,
4256
4450
  timeout: 3e5
4257
4451
  });
4258
- logger19.info("Dependencies installed");
4452
+ logger20.info("Dependencies installed");
4259
4453
  } catch (err) {
4260
4454
  if (fallbackCmd) {
4261
- logger19.warn(`${installCmd} failed, retrying with fallback command`, {
4455
+ logger20.warn(`${installCmd} failed, retrying with fallback command`, {
4262
4456
  error: err.message
4263
4457
  });
4264
4458
  const [fallbackBin, ...fallbackArgs] = fallbackCmd.split(/\s+/);
@@ -4268,45 +4462,45 @@ var PipelineOrchestrator = class {
4268
4462
  maxBuffer: 10 * 1024 * 1024,
4269
4463
  timeout: 3e5
4270
4464
  });
4271
- logger19.info("Dependencies installed (fallback)");
4465
+ logger20.info("Dependencies installed (fallback)");
4272
4466
  } catch (retryErr) {
4273
- logger19.warn("Fallback install also failed", {
4467
+ logger20.warn("Fallback install also failed", {
4274
4468
  error: retryErr.message
4275
4469
  });
4276
4470
  }
4277
4471
  } else {
4278
- logger19.warn("Install failed, no fallback configured", {
4472
+ logger20.warn("Install failed, no fallback configured", {
4279
4473
  error: err.message
4280
4474
  });
4281
4475
  }
4282
4476
  }
4283
4477
  }
4284
4478
  async ensureNodeModules(workDir) {
4285
- const targetBin = path13.join(workDir, "node_modules", ".bin");
4479
+ const targetBin = path14.join(workDir, "node_modules", ".bin");
4286
4480
  try {
4287
- await fs12.access(targetBin);
4288
- logger19.info("node_modules already complete (has .bin/)");
4481
+ await fs13.access(targetBin);
4482
+ logger20.info("node_modules already complete (has .bin/)");
4289
4483
  return true;
4290
4484
  } catch {
4291
4485
  }
4292
- const sourceNM = path13.join(this.config.project.workDir, "node_modules");
4293
- 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");
4294
4488
  try {
4295
- await fs12.access(sourceNM);
4489
+ await fs13.access(sourceNM);
4296
4490
  } catch {
4297
- logger19.warn("Main repo node_modules not found, skipping seed", { sourceNM });
4491
+ logger20.warn("Main repo node_modules not found, skipping seed", { sourceNM });
4298
4492
  return false;
4299
4493
  }
4300
- 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 });
4301
4495
  try {
4302
4496
  await execFileAsync2("rm", ["-rf", targetNM], { timeout: 6e4 });
4303
4497
  await execFileAsync2("cp", ["-a", "--reflink=auto", sourceNM, targetNM], {
4304
4498
  timeout: 12e4
4305
4499
  });
4306
- logger19.info("node_modules seeded from main repo");
4500
+ logger20.info("node_modules seeded from main repo");
4307
4501
  return true;
4308
4502
  } catch (err) {
4309
- logger19.warn("Failed to seed node_modules from main repo", {
4503
+ logger20.warn("Failed to seed node_modules from main repo", {
4310
4504
  error: err.message
4311
4505
  });
4312
4506
  return false;
@@ -4316,14 +4510,14 @@ var PipelineOrchestrator = class {
4316
4510
  const record = this.tracker.get(issueIid);
4317
4511
  if (!record) throw new IssueNotFoundError(issueIid);
4318
4512
  const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
4319
- logger19.info("Restarting issue \u2014 cleaning context", { issueIid, branchName: record.branchName });
4513
+ logger20.info("Restarting issue \u2014 cleaning context", { issueIid, branchName: record.branchName });
4320
4514
  this.aiRunner.killByWorkDir(wtCtx.workDir);
4321
4515
  this.stopPreviewServers(issueIid);
4322
4516
  try {
4323
4517
  const deleted = await this.gongfeng.cleanupAgentNotes(getExternalId(record));
4324
- logger19.info("Agent notes cleaned up", { issueIid, deleted });
4518
+ logger20.info("Agent notes cleaned up", { issueIid, deleted });
4325
4519
  } catch (err) {
4326
- logger19.warn("Failed to cleanup agent notes", { issueIid, error: err.message });
4520
+ logger20.warn("Failed to cleanup agent notes", { issueIid, error: err.message });
4327
4521
  }
4328
4522
  await this.mainGitMutex.runExclusive(async () => {
4329
4523
  await this.cleanupWorktree(wtCtx);
@@ -4337,19 +4531,19 @@ var PipelineOrchestrator = class {
4337
4531
  }
4338
4532
  });
4339
4533
  this.tracker.resetFull(issueIid);
4340
- logger19.info("Issue restarted", { issueIid });
4534
+ logger20.info("Issue restarted", { issueIid });
4341
4535
  }
4342
4536
  async cancelIssue(issueIid) {
4343
4537
  const record = this.tracker.get(issueIid);
4344
4538
  if (!record) throw new IssueNotFoundError(issueIid);
4345
4539
  const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
4346
- 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 });
4347
4541
  this.aiRunner.killByWorkDir(wtCtx.workDir);
4348
4542
  this.stopPreviewServers(issueIid);
4349
4543
  try {
4350
4544
  await this.gongfeng.removeLabelsWithPrefix(getExternalId(record), "auto-finish");
4351
4545
  } catch (err) {
4352
- 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 });
4353
4547
  }
4354
4548
  await this.mainGitMutex.runExclusive(async () => {
4355
4549
  await this.cleanupWorktree(wtCtx);
@@ -4363,7 +4557,7 @@ var PipelineOrchestrator = class {
4363
4557
  }
4364
4558
  });
4365
4559
  this.tracker.updateState(issueIid, "skipped" /* Skipped */);
4366
- logger19.info("Issue cancelled", { issueIid });
4560
+ logger20.info("Issue cancelled", { issueIid });
4367
4561
  }
4368
4562
  retryFromPhase(issueIid, phase) {
4369
4563
  const record = this.tracker.get(issueIid);
@@ -4373,12 +4567,107 @@ var PipelineOrchestrator = class {
4373
4567
  if (!issueLM.isRetryable(phase)) {
4374
4568
  throw new InvalidPhaseError(phase);
4375
4569
  }
4376
- logger19.info("Retrying issue from phase", { issueIid, phase });
4570
+ logger20.info("Retrying issue from phase", { issueIid, phase });
4377
4571
  const ok = this.tracker.resetToPhase(issueIid, phase, issueDef);
4378
4572
  if (!ok) {
4379
4573
  throw new InvalidPhaseError(phase);
4380
4574
  }
4381
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
+ }
4382
4671
  getIssueSpecificPipelineDef(record) {
4383
4672
  if (record.pipelineMode) {
4384
4673
  return getPipelineDef(record.pipelineMode);
@@ -4414,13 +4703,20 @@ var PipelineOrchestrator = class {
4414
4703
  startPreviewServers: (wtCtx, issue) => this.startPreviewServers(wtCtx, issue),
4415
4704
  stopPreviewServers: (iid) => this.stopPreviewServers(iid),
4416
4705
  tryCreateMergeRequest: (issue, branch, workDir, previewUrl) => this.tryCreateMergeRequest(issue, branch, workDir, previewUrl),
4417
- buildPreviewUrl: (iid) => this.buildPreviewUrl(iid)
4706
+ buildPreviewUrl: (iid) => this.buildPreviewUrl(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
+ }
4418
4714
  };
4419
4715
  }
4420
4716
  async _processIssueImpl(issue) {
4421
4717
  const branchName = `${this.config.project.branchPrefix}-${issue.iid}`;
4422
4718
  const wtCtx = this.computeWorktreeContext(issue.iid, branchName);
4423
- logger19.info("Processing issue", {
4719
+ logger20.info("Processing issue", {
4424
4720
  iid: issue.iid,
4425
4721
  title: issue.title,
4426
4722
  branchName,
@@ -4454,6 +4750,9 @@ var PipelineOrchestrator = class {
4454
4750
  branchName,
4455
4751
  pipelineMode: issuePipelineDef.mode
4456
4752
  };
4753
+ if (record.ports) {
4754
+ phaseCtx.ports = record.ports;
4755
+ }
4457
4756
  const ctx = {
4458
4757
  issue,
4459
4758
  branchName,
@@ -4484,6 +4783,16 @@ var PipelineOrchestrator = class {
4484
4783
  if (phaseResult.paused) return;
4485
4784
  await executeCompletion(ctx, deps, phaseResult, wtGitMap);
4486
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
+ }
4487
4796
  await handleFailure(err, issue, wtCtx, deps);
4488
4797
  }
4489
4798
  }
@@ -4512,7 +4821,7 @@ var PipelineOrchestrator = class {
4512
4821
  title,
4513
4822
  description
4514
4823
  });
4515
- logger19.info("Merge request created successfully", {
4824
+ logger20.info("Merge request created successfully", {
4516
4825
  iid: issue.iid,
4517
4826
  mrIid: mr.iid,
4518
4827
  mrUrl: mr.web_url
@@ -4520,7 +4829,7 @@ var PipelineOrchestrator = class {
4520
4829
  return { url: mr.web_url, iid: mr.iid };
4521
4830
  } catch (err) {
4522
4831
  const errorMsg = err.message;
4523
- 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", {
4524
4833
  iid: issue.iid,
4525
4834
  error: errorMsg
4526
4835
  });
@@ -4537,7 +4846,7 @@ var PipelineOrchestrator = class {
4537
4846
  this.config.project.baseBranch
4538
4847
  );
4539
4848
  if (existing) {
4540
- logger19.info("Found existing merge request", {
4849
+ logger20.info("Found existing merge request", {
4541
4850
  iid: issueIid,
4542
4851
  mrIid: existing.iid,
4543
4852
  mrUrl: existing.web_url
@@ -4545,7 +4854,7 @@ var PipelineOrchestrator = class {
4545
4854
  return { url: existing.web_url, iid: existing.iid };
4546
4855
  }
4547
4856
  } catch (findErr) {
4548
- logger19.warn("Failed to find existing merge request", {
4857
+ logger20.warn("Failed to find existing merge request", {
4549
4858
  iid: issueIid,
4550
4859
  error: findErr.message
4551
4860
  });
@@ -4579,6 +4888,11 @@ var PipelineOrchestrator = class {
4579
4888
  } catch {
4580
4889
  }
4581
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
+ },
4582
4896
  isNoteSyncEnabled: () => isNoteSyncEnabledForIssue(iid, this.tracker, this.config),
4583
4897
  isE2eEnabled: () => isE2eEnabledForIssue(iid, this.tracker, this.config)
4584
4898
  };
@@ -4611,7 +4925,7 @@ var PipelineOrchestrator = class {
4611
4925
  });
4612
4926
  return ports;
4613
4927
  } catch (err) {
4614
- logger19.error("Failed to start preview servers", {
4928
+ logger20.error("Failed to start preview servers", {
4615
4929
  iid: issue.iid,
4616
4930
  error: err.message
4617
4931
  });
@@ -4646,7 +4960,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4646
4960
  await this.mainGitMutex.runExclusive(async () => {
4647
4961
  await this.cleanupWorktree(wtCtx);
4648
4962
  });
4649
- logger19.info("Preview stopped and worktree cleaned", { iid: issueIid });
4963
+ logger20.info("Preview stopped and worktree cleaned", { iid: issueIid });
4650
4964
  }
4651
4965
  async markDeployed(issueIid) {
4652
4966
  const record = this.tracker.get(issueIid);
@@ -4663,7 +4977,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4663
4977
  try {
4664
4978
  await this.gongfeng.closeIssue(externalId);
4665
4979
  } catch (err) {
4666
- 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 });
4667
4981
  }
4668
4982
  try {
4669
4983
  const issue = await this.gongfeng.getIssueDetail(externalId);
@@ -4671,10 +4985,10 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4671
4985
  labels.push("auto-finish:deployed");
4672
4986
  await this.gongfeng.updateIssueLabels(externalId, labels);
4673
4987
  } catch (err) {
4674
- logger19.warn("Failed to update labels", { iid: issueIid, error: err.message });
4988
+ logger20.warn("Failed to update labels", { iid: issueIid, error: err.message });
4675
4989
  }
4676
4990
  this.tracker.updateState(issueIid, "deployed" /* Deployed */);
4677
- logger19.info("Issue marked as deployed", { iid: issueIid });
4991
+ logger20.info("Issue marked as deployed", { iid: issueIid });
4678
4992
  }
4679
4993
  async restartPreview(issueIid) {
4680
4994
  const record = this.tracker.get(issueIid);
@@ -4688,7 +5002,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4688
5002
  wtCtx.ports = ports;
4689
5003
  try {
4690
5004
  this.tracker.updateState(issueIid, record.state, {
4691
- ports: { frontend: ports.frontendPort, backend: ports.backendPort },
5005
+ ports,
4692
5006
  previewStartedAt: (/* @__PURE__ */ new Date()).toISOString()
4693
5007
  });
4694
5008
  await this.devServerManager.startServers(wtCtx, ports);
@@ -4701,7 +5015,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4701
5015
  throw err;
4702
5016
  }
4703
5017
  const url = this.buildPreviewUrl(issueIid);
4704
- logger19.info("Preview restarted", { iid: issueIid, url });
5018
+ logger20.info("Preview restarted", { iid: issueIid, url });
4705
5019
  return url;
4706
5020
  }
4707
5021
  getPreviewHost() {
@@ -4734,7 +5048,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4734
5048
  if (!record) throw new IssueNotFoundError(issueIid);
4735
5049
  const baseBranch = this.config.project.baseBranch;
4736
5050
  const branchName = record.branchName;
4737
- logger19.info("Starting conflict resolution", { issueIid, branchName, baseBranch });
5051
+ logger20.info("Starting conflict resolution", { issueIid, branchName, baseBranch });
4738
5052
  this.tracker.updateState(issueIid, "resolving_conflict" /* ResolvingConflict */);
4739
5053
  this.eventBus.emitTyped("conflict:started", { issueIid });
4740
5054
  try {
@@ -4767,7 +5081,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4767
5081
  });
4768
5082
  }
4769
5083
  });
4770
- logger19.info("Running verification after conflict resolution", { issueIid });
5084
+ logger20.info("Running verification after conflict resolution", { issueIid });
4771
5085
  const wtPlan = new PlanPersistence(wtCtx.workDir, issueIid);
4772
5086
  wtPlan.ensureDir();
4773
5087
  const conflictLM = createLifecycleManager(this.getIssueSpecificPipelineDef(record));
@@ -4800,10 +5114,10 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4800
5114
  } catch {
4801
5115
  }
4802
5116
  await this.commentOnMr(record.mrUrl, t("conflict.mrResolvedComment"));
4803
- logger19.info("Conflict resolution completed", { issueIid });
5117
+ logger20.info("Conflict resolution completed", { issueIid });
4804
5118
  } catch (err) {
4805
5119
  const errorMsg = err.message;
4806
- logger19.error("Conflict resolution failed", { issueIid, error: errorMsg });
5120
+ logger20.error("Conflict resolution failed", { issueIid, error: errorMsg });
4807
5121
  try {
4808
5122
  const wtGit = new GitOperations(wtCtx.gitRootDir);
4809
5123
  if (await wtGit.isRebaseInProgress()) {
@@ -4833,7 +5147,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4833
5147
  try {
4834
5148
  await this.gongfeng.createMergeRequestNote(mrIid, body);
4835
5149
  } catch (err) {
4836
- logger19.warn("Failed to comment on MR", { mrIid, error: err.message });
5150
+ logger20.warn("Failed to comment on MR", { mrIid, error: err.message });
4837
5151
  }
4838
5152
  }
4839
5153
  };
@@ -4909,7 +5223,7 @@ ${questions}
4909
5223
  }
4910
5224
 
4911
5225
  // src/services/BrainstormService.ts
4912
- var logger20 = logger.child("Brainstorm");
5226
+ var logger21 = logger.child("Brainstorm");
4913
5227
  function agentConfigToAIConfig(agentCfg, timeoutMs) {
4914
5228
  return {
4915
5229
  mode: agentCfg.mode,
@@ -4945,7 +5259,7 @@ var BrainstormService = class {
4945
5259
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
4946
5260
  };
4947
5261
  this.sessions.set(session.id, session);
4948
- logger20.info("Created brainstorm session", { sessionId: session.id });
5262
+ logger21.info("Created brainstorm session", { sessionId: session.id });
4949
5263
  return session;
4950
5264
  }
4951
5265
  getSession(id) {
@@ -4954,7 +5268,7 @@ var BrainstormService = class {
4954
5268
  async generate(sessionId, onEvent) {
4955
5269
  const session = this.requireSession(sessionId);
4956
5270
  session.status = "generating";
4957
- logger20.info("Generating SDD", { sessionId });
5271
+ logger21.info("Generating SDD", { sessionId });
4958
5272
  const prompt = buildGeneratePrompt(session.transcript);
4959
5273
  const result = await this.generatorRunner.run({
4960
5274
  prompt,
@@ -4980,7 +5294,7 @@ var BrainstormService = class {
4980
5294
  const session = this.requireSession(sessionId);
4981
5295
  const roundNum = session.rounds.length + 1;
4982
5296
  session.status = "reviewing";
4983
- logger20.info("Reviewing SDD", { sessionId, round: roundNum });
5297
+ logger21.info("Reviewing SDD", { sessionId, round: roundNum });
4984
5298
  onEvent?.({ type: "round:start", data: { round: roundNum, phase: "review" }, round: roundNum });
4985
5299
  const prompt = buildReviewPrompt(session.currentSdd, roundNum);
4986
5300
  const result = await this.reviewerRunner.run({
@@ -5013,7 +5327,7 @@ var BrainstormService = class {
5013
5327
  throw new Error("No review round to refine from");
5014
5328
  }
5015
5329
  session.status = "refining";
5016
- logger20.info("Refining SDD", { sessionId, round: currentRound.round });
5330
+ logger21.info("Refining SDD", { sessionId, round: currentRound.round });
5017
5331
  const prompt = buildRefinePrompt(currentRound.questions);
5018
5332
  const result = await this.generatorRunner.run({
5019
5333
  prompt,
@@ -5098,4 +5412,4 @@ export {
5098
5412
  PipelineOrchestrator,
5099
5413
  BrainstormService
5100
5414
  };
5101
- //# sourceMappingURL=chunk-UDCMSDNT.js.map
5415
+ //# sourceMappingURL=chunk-5UPYA6KH.js.map