@xdevops/issue-auto-finish 1.0.85 → 1.0.87

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 (129) hide show
  1. package/dist/AIRunnerRegistry-II3WWSFN.js +31 -0
  2. package/dist/PtyRunner-6UGI5STW.js +22 -0
  3. package/dist/TerminalManager-RT2N7N5R.js +8 -0
  4. package/dist/ai-runner/AIRunner.d.ts +9 -1
  5. package/dist/ai-runner/AIRunner.d.ts.map +1 -1
  6. package/dist/ai-runner/AIRunnerRegistry.d.ts +37 -1
  7. package/dist/ai-runner/AIRunnerRegistry.d.ts.map +1 -1
  8. package/dist/ai-runner/PtyRunner.d.ts +114 -0
  9. package/dist/ai-runner/PtyRunner.d.ts.map +1 -0
  10. package/dist/ai-runner/index.d.ts +3 -1
  11. package/dist/ai-runner/index.d.ts.map +1 -1
  12. package/dist/{ai-runner-SVUNA3FX.js → ai-runner-HLA44WI6.js} +12 -3
  13. package/dist/{analyze-SXXPE5XL.js → analyze-ZIXNC5GN.js} +10 -8
  14. package/dist/{analyze-SXXPE5XL.js.map → analyze-ZIXNC5GN.js.map} +1 -1
  15. package/dist/{braindump-4E5SDMSZ.js → braindump-56WAY2RD.js} +10 -8
  16. package/dist/{braindump-4E5SDMSZ.js.map → braindump-56WAY2RD.js.map} +1 -1
  17. package/dist/{chunk-JINMYD56.js → chunk-2MESXJEZ.js} +3 -3
  18. package/dist/{chunk-P4O4ZXEC.js → chunk-2YQHKXLL.js} +40 -19
  19. package/dist/chunk-2YQHKXLL.js.map +1 -0
  20. package/dist/chunk-AVGZH64A.js +211 -0
  21. package/dist/chunk-AVGZH64A.js.map +1 -0
  22. package/dist/{chunk-5UPYA6KH.js → chunk-IP3QTP5A.js} +1028 -763
  23. package/dist/chunk-IP3QTP5A.js.map +1 -0
  24. package/dist/chunk-KC5S66OZ.js +177 -0
  25. package/dist/chunk-KC5S66OZ.js.map +1 -0
  26. package/dist/{chunk-4QV6D34Y.js → chunk-M5C2WILQ.js} +8 -6
  27. package/dist/{chunk-4QV6D34Y.js.map → chunk-M5C2WILQ.js.map} +1 -1
  28. package/dist/{chunk-FWEW5E3B.js → chunk-NZHKAPU6.js} +35 -5
  29. package/dist/chunk-NZHKAPU6.js.map +1 -0
  30. package/dist/{chunk-KTYPZTF4.js → chunk-O3WEV5W3.js} +10 -2
  31. package/dist/chunk-O3WEV5W3.js.map +1 -0
  32. package/dist/{chunk-K2OTLYJI.js → chunk-QZZGIZWC.js} +457 -202
  33. package/dist/chunk-QZZGIZWC.js.map +1 -0
  34. package/dist/{chunk-4LFNFRCL.js → chunk-SAMTXC4A.js} +91 -214
  35. package/dist/chunk-SAMTXC4A.js.map +1 -0
  36. package/dist/chunk-U237JSLB.js +1 -0
  37. package/dist/chunk-U237JSLB.js.map +1 -0
  38. package/dist/chunk-U6GWFTKA.js +657 -0
  39. package/dist/chunk-U6GWFTKA.js.map +1 -0
  40. package/dist/{chunk-HOFYJEJ4.js → chunk-UBQLXQ7I.js} +11 -11
  41. package/dist/cli/setup/env-metadata.d.ts.map +1 -1
  42. package/dist/cli.js +8 -7
  43. package/dist/cli.js.map +1 -1
  44. package/dist/{config-QLINHCHD.js → config-WTRSZLOC.js} +4 -3
  45. package/dist/config-WTRSZLOC.js.map +1 -0
  46. package/dist/config-schema.d.ts +17 -1
  47. package/dist/config-schema.d.ts.map +1 -1
  48. package/dist/config.d.ts +20 -0
  49. package/dist/config.d.ts.map +1 -1
  50. package/dist/errors/PhaseAbortedError.d.ts +3 -3
  51. package/dist/errors/PhaseAbortedError.d.ts.map +1 -1
  52. package/dist/errors-S3BWYA4I.js +43 -0
  53. package/dist/errors-S3BWYA4I.js.map +1 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +14 -11
  56. package/dist/{init-TDKDC6YP.js → init-QQDXGTPB.js} +7 -6
  57. package/dist/{init-TDKDC6YP.js.map → init-QQDXGTPB.js.map} +1 -1
  58. package/dist/lib.js +9 -7
  59. package/dist/lib.js.map +1 -1
  60. package/dist/orchestrator/IssueProcessingContext.d.ts +39 -21
  61. package/dist/orchestrator/IssueProcessingContext.d.ts.map +1 -1
  62. package/dist/orchestrator/PipelineOrchestrator.d.ts +10 -1
  63. package/dist/orchestrator/PipelineOrchestrator.d.ts.map +1 -1
  64. package/dist/orchestrator/steps/PhaseLoopStep.d.ts +1 -1
  65. package/dist/orchestrator/steps/PhaseLoopStep.d.ts.map +1 -1
  66. package/dist/orchestrator/steps/SetupStep.d.ts.map +1 -1
  67. package/dist/persistence/PlanPersistence.d.ts +7 -1
  68. package/dist/persistence/PlanPersistence.d.ts.map +1 -1
  69. package/dist/phases/BasePhase.d.ts +31 -42
  70. package/dist/phases/BasePhase.d.ts.map +1 -1
  71. package/dist/phases/BuildPhase.d.ts.map +1 -1
  72. package/dist/phases/PhaseFactory.d.ts +2 -3
  73. package/dist/phases/PhaseFactory.d.ts.map +1 -1
  74. package/dist/phases/PhaseOutcome.d.ts +42 -0
  75. package/dist/phases/PhaseOutcome.d.ts.map +1 -0
  76. package/dist/phases/PlanPhase.d.ts +1 -1
  77. package/dist/phases/PlanPhase.d.ts.map +1 -1
  78. package/dist/phases/ReleasePhase.d.ts +8 -18
  79. package/dist/phases/ReleasePhase.d.ts.map +1 -1
  80. package/dist/phases/UatPhase.d.ts +7 -24
  81. package/dist/phases/UatPhase.d.ts.map +1 -1
  82. package/dist/phases/VerifyPhase.d.ts +4 -4
  83. package/dist/phases/VerifyPhase.d.ts.map +1 -1
  84. package/dist/poller/IssuePoller.d.ts.map +1 -1
  85. package/dist/prompts/release-templates.d.ts.map +1 -1
  86. package/dist/prompts/templates.d.ts.map +1 -1
  87. package/dist/{restart-RNXGTDWZ.js → restart-BMILTP5X.js} +6 -5
  88. package/dist/{restart-RNXGTDWZ.js.map → restart-BMILTP5X.js.map} +1 -1
  89. package/dist/run.js +14 -11
  90. package/dist/run.js.map +1 -1
  91. package/dist/settings/ExperimentalSettings.d.ts +1 -1
  92. package/dist/settings/ExperimentalSettings.d.ts.map +1 -1
  93. package/dist/start-6QRW6IJI.js +15 -0
  94. package/dist/start-6QRW6IJI.js.map +1 -0
  95. package/dist/terminal/TerminalManager.d.ts +62 -0
  96. package/dist/terminal/TerminalManager.d.ts.map +1 -0
  97. package/dist/terminal/TerminalWebSocket.d.ts +9 -0
  98. package/dist/terminal/TerminalWebSocket.d.ts.map +1 -0
  99. package/dist/tracker/ExecutableTask.d.ts +4 -2
  100. package/dist/tracker/ExecutableTask.d.ts.map +1 -1
  101. package/dist/tracker/IssueState.d.ts +11 -1
  102. package/dist/tracker/IssueState.d.ts.map +1 -1
  103. package/dist/tracker/IssueTracker.d.ts +19 -1
  104. package/dist/tracker/IssueTracker.d.ts.map +1 -1
  105. package/dist/web/WebServer.d.ts +4 -0
  106. package/dist/web/WebServer.d.ts.map +1 -1
  107. package/dist/web/routes/terminal.d.ts +11 -0
  108. package/dist/web/routes/terminal.d.ts.map +1 -0
  109. package/dist/webhook/CommandExecutor.d.ts.map +1 -1
  110. package/package.json +7 -1
  111. package/src/web/frontend/dist/assets/index-COYziOhv.css +1 -0
  112. package/src/web/frontend/dist/assets/index-D_oTMuJU.js +151 -0
  113. package/src/web/frontend/dist/index.html +2 -2
  114. package/dist/chunk-4LFNFRCL.js.map +0 -1
  115. package/dist/chunk-5UPYA6KH.js.map +0 -1
  116. package/dist/chunk-DADQSKPL.js +0 -1
  117. package/dist/chunk-FWEW5E3B.js.map +0 -1
  118. package/dist/chunk-K2OTLYJI.js.map +0 -1
  119. package/dist/chunk-KTYPZTF4.js.map +0 -1
  120. package/dist/chunk-P4O4ZXEC.js.map +0 -1
  121. package/dist/start-27GRO4DP.js +0 -14
  122. package/src/web/frontend/dist/assets/index-C4NXoH9S.js +0 -133
  123. package/src/web/frontend/dist/assets/index-C7lorIa0.css +0 -1
  124. /package/dist/{ai-runner-SVUNA3FX.js.map → AIRunnerRegistry-II3WWSFN.js.map} +0 -0
  125. /package/dist/{chunk-DADQSKPL.js.map → PtyRunner-6UGI5STW.js.map} +0 -0
  126. /package/dist/{config-QLINHCHD.js.map → TerminalManager-RT2N7N5R.js.map} +0 -0
  127. /package/dist/{start-27GRO4DP.js.map → ai-runner-HLA44WI6.js.map} +0 -0
  128. /package/dist/{chunk-JINMYD56.js.map → chunk-2MESXJEZ.js.map} +0 -0
  129. /package/dist/{chunk-HOFYJEJ4.js.map → chunk-UBQLXQ7I.js.map} +0 -0
@@ -17,22 +17,27 @@ import {
17
17
  planPrompt,
18
18
  rePlanPrompt,
19
19
  verifyPrompt
20
- } from "./chunk-P4O4ZXEC.js";
20
+ } from "./chunk-2YQHKXLL.js";
21
21
  import {
22
22
  getProjectKnowledge
23
23
  } from "./chunk-ACVOOHAR.js";
24
+ import {
25
+ KNOWLEDGE_DEFAULTS
26
+ } from "./chunk-B7TVVODN.js";
24
27
  import {
25
28
  t
26
29
  } from "./chunk-YCYVNRLF.js";
27
30
  import {
28
31
  getLocalIP
29
32
  } from "./chunk-AKXDQH25.js";
30
- import {
31
- KNOWLEDGE_DEFAULTS
32
- } from "./chunk-B7TVVODN.js";
33
33
  import {
34
34
  resolveDataDir
35
35
  } from "./chunk-TN2SYADO.js";
36
+ import {
37
+ createAIRunner,
38
+ getRunnerCapabilities,
39
+ isShuttingDown
40
+ } from "./chunk-SAMTXC4A.js";
36
41
  import {
37
42
  AIExecutionError,
38
43
  GongfengApiError,
@@ -45,11 +50,8 @@ import {
45
50
  PipelineNotFoundError,
46
51
  PortExhaustionError,
47
52
  ServiceShutdownError,
48
- UnregisteredPhasesError,
49
- createAIRunner,
50
- getRunnerCapabilities,
51
- isShuttingDown
52
- } from "./chunk-4LFNFRCL.js";
53
+ UnregisteredPhasesError
54
+ } from "./chunk-AVGZH64A.js";
53
55
  import {
54
56
  logger,
55
57
  runWithIssueContext
@@ -228,8 +230,8 @@ var GongfengClient = class {
228
230
  const encoded = encodeURIComponent(this.projectPath);
229
231
  return `${this.apiUrl}/api/v3/projects/${encoded}`;
230
232
  }
231
- async requestRaw(path15, options = {}) {
232
- const url = `${this.projectApiBase}${path15}`;
233
+ async requestRaw(path12, options = {}) {
234
+ const url = `${this.projectApiBase}${path12}`;
233
235
  logger4.debug("API request", { method: options.method || "GET", url });
234
236
  return this.circuitBreaker.execute(
235
237
  () => this.retryPolicy.execute(async () => {
@@ -246,11 +248,11 @@ var GongfengClient = class {
246
248
  throw new GongfengApiError(resp.status, `Gongfeng API error ${resp.status}: ${body}`, body);
247
249
  }
248
250
  return resp;
249
- }, `requestRaw ${options.method || "GET"} ${path15}`)
251
+ }, `requestRaw ${options.method || "GET"} ${path12}`)
250
252
  );
251
253
  }
252
- async request(path15, options = {}) {
253
- const resp = await this.requestRaw(path15, options);
254
+ async request(path12, options = {}) {
255
+ const resp = await this.requestRaw(path12, options);
254
256
  return resp.json();
255
257
  }
256
258
  async createIssue(title, description, labels) {
@@ -429,8 +431,8 @@ var GongfengClient = class {
429
431
  }
430
432
  return mr;
431
433
  }
432
- async requestGlobal(path15, options = {}) {
433
- const url = `${this.apiUrl}${path15}`;
434
+ async requestGlobal(path12, options = {}) {
435
+ const url = `${this.apiUrl}${path12}`;
434
436
  logger4.debug("API request (global)", { method: options.method || "GET", url });
435
437
  const resp = await this.circuitBreaker.execute(
436
438
  () => this.retryPolicy.execute(async () => {
@@ -447,7 +449,7 @@ var GongfengClient = class {
447
449
  throw new GongfengApiError(r.status, `Gongfeng API error ${r.status}: ${body}`, body);
448
450
  }
449
451
  return r;
450
- }, `requestGlobal ${options.method || "GET"} ${path15}`)
452
+ }, `requestGlobal ${options.method || "GET"} ${path12}`)
451
453
  );
452
454
  return resp.json();
453
455
  }
@@ -934,7 +936,7 @@ var STATE_MIGRATION_MAP = {
934
936
  waiting_for_review: { state: "phase_waiting", currentPhase: "review" },
935
937
  review_approved: { state: "phase_approved", currentPhase: "review" }
936
938
  };
937
- var IssueTracker = class extends BaseTracker {
939
+ var IssueTracker = class _IssueTracker extends BaseTracker {
938
940
  lifecycleManagers;
939
941
  tenantId;
940
942
  constructor(dataDir, lifecycleManagers, tenantId) {
@@ -1044,6 +1046,29 @@ var IssueTracker = class extends BaseTracker {
1044
1046
  logger5.info("Issue state updated", { issueIid, state });
1045
1047
  eventBus.emitTyped("issue:stateChanged", { issueIid, state, record });
1046
1048
  }
1049
+ /** 初始化阶段进度(流水线启动时调用) */
1050
+ initPhaseProgress(issueIid, def) {
1051
+ const record = this.collection[this.key(issueIid)];
1052
+ if (!record) return;
1053
+ const phases = {};
1054
+ for (const spec of def.phases) {
1055
+ phases[spec.name] = { status: "pending" };
1056
+ }
1057
+ record.phaseProgress = phases;
1058
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1059
+ this.save();
1060
+ }
1061
+ /** 更新单个阶段的进度(原子保存 + SSE 推送) */
1062
+ updatePhaseProgress(issueIid, phase, update) {
1063
+ const record = this.collection[this.key(issueIid)];
1064
+ if (!record?.phaseProgress) return;
1065
+ const pp = record.phaseProgress[phase];
1066
+ if (!pp) return;
1067
+ Object.assign(pp, update);
1068
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1069
+ this.save();
1070
+ eventBus.emitTyped("issue:stateChanged", { issueIid, state: record.state, record });
1071
+ }
1047
1072
  markFailed(issueIid, error, failedAtState, isRetryable) {
1048
1073
  const record = this.collection[this.key(issueIid)];
1049
1074
  if (!record) return;
@@ -1064,6 +1089,7 @@ var IssueTracker = class extends BaseTracker {
1064
1089
  record.pausedAtPhase = currentPhase;
1065
1090
  record.failedAtState = void 0;
1066
1091
  record.lastError = void 0;
1092
+ record.processingLock = void 0;
1067
1093
  record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1068
1094
  this.save();
1069
1095
  logger5.info("Issue paused", { issueIid, pausedAtPhase: currentPhase });
@@ -1092,6 +1118,65 @@ var IssueTracker = class extends BaseTracker {
1092
1118
  eventBus.emitTyped(eventType, { issueIid, phase, record });
1093
1119
  return true;
1094
1120
  }
1121
+ // ── processingLock 管理 ──
1122
+ /** 持久化锁超时阈值(默认 30 分钟,与 PHASE_TIMEOUT 一致) */
1123
+ static LOCK_TIMEOUT_MS = 30 * 60 * 1e3;
1124
+ /**
1125
+ * 获取持久化处理锁。
1126
+ * - 无锁或超时锁 → 写入并返回 true
1127
+ * - 有未超时锁(其他 correlationId)→ 返回 false
1128
+ */
1129
+ acquireProcessingLock(issueIid, correlationId) {
1130
+ const record = this.collection[this.key(issueIid)];
1131
+ if (!record) return false;
1132
+ const existing = record.processingLock;
1133
+ if (existing) {
1134
+ const age = Date.now() - new Date(existing.ts).getTime();
1135
+ if (age < _IssueTracker.LOCK_TIMEOUT_MS) {
1136
+ logger5.warn("Processing lock held, rejecting acquire", {
1137
+ issueIid,
1138
+ existingCorrelationId: existing.correlationId,
1139
+ ageMs: age
1140
+ });
1141
+ return false;
1142
+ }
1143
+ logger5.warn("Processing lock timed out, forcibly acquiring", {
1144
+ issueIid,
1145
+ staleCorrelationId: existing.correlationId,
1146
+ ageMs: age
1147
+ });
1148
+ }
1149
+ record.processingLock = { correlationId, ts: (/* @__PURE__ */ new Date()).toISOString() };
1150
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1151
+ this.save();
1152
+ return true;
1153
+ }
1154
+ /**
1155
+ * 释放持久化处理锁。仅当 correlationId 匹配时才清除,防止旧协程误释放新锁。
1156
+ */
1157
+ releaseProcessingLock(issueIid, correlationId) {
1158
+ const record = this.collection[this.key(issueIid)];
1159
+ if (!record?.processingLock) return;
1160
+ if (record.processingLock.correlationId !== correlationId) {
1161
+ logger5.warn("Processing lock correlationId mismatch, skipping release", {
1162
+ issueIid,
1163
+ ownCorrelationId: correlationId,
1164
+ actualCorrelationId: record.processingLock.correlationId
1165
+ });
1166
+ return;
1167
+ }
1168
+ record.processingLock = void 0;
1169
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1170
+ this.save();
1171
+ }
1172
+ /** 强制清除持久化处理锁(管理员操作:restart/cancel/retryFromPhase) */
1173
+ clearProcessingLock(issueIid) {
1174
+ const record = this.collection[this.key(issueIid)];
1175
+ if (!record?.processingLock) return;
1176
+ record.processingLock = void 0;
1177
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1178
+ this.save();
1179
+ }
1095
1180
  isProcessing(issueIid) {
1096
1181
  const record = this.get(issueIid);
1097
1182
  if (!record) return false;
@@ -1121,9 +1206,15 @@ var IssueTracker = class extends BaseTracker {
1121
1206
  getDrivableIssues(maxRetries, stalledThresholdMs) {
1122
1207
  return this.getAllRecords().filter((record) => {
1123
1208
  const lm = this.lifecycleFor(record);
1124
- if (lm.isDrivable(record.state, record.attempts, maxRetries, record.lastErrorRetryable)) return true;
1125
- if (this.isStalled(getIid(record), stalledThresholdMs)) return true;
1126
- return false;
1209
+ const drivable = lm.isDrivable(record.state, record.attempts, maxRetries, record.lastErrorRetryable) || this.isStalled(getIid(record), stalledThresholdMs);
1210
+ if (!drivable) return false;
1211
+ if (record.processingLock) {
1212
+ const age = Date.now() - new Date(record.processingLock.ts).getTime();
1213
+ if (age < _IssueTracker.LOCK_TIMEOUT_MS) {
1214
+ return false;
1215
+ }
1216
+ }
1217
+ return true;
1127
1218
  });
1128
1219
  }
1129
1220
  getAllActive() {
@@ -1152,6 +1243,8 @@ var IssueTracker = class extends BaseTracker {
1152
1243
  record.attempts = 0;
1153
1244
  record.failedAtState = void 0;
1154
1245
  record.lastError = void 0;
1246
+ record.phaseProgress = void 0;
1247
+ record.processingLock = void 0;
1155
1248
  record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1156
1249
  this.save();
1157
1250
  logger5.info("Issue fully reset", { issueIid });
@@ -1174,6 +1267,21 @@ var IssueTracker = class extends BaseTracker {
1174
1267
  }
1175
1268
  record.failedAtState = void 0;
1176
1269
  record.lastError = void 0;
1270
+ record.processingLock = void 0;
1271
+ if (record.phaseProgress) {
1272
+ const phaseIdx = def.phases.findIndex((p) => p.name === phase);
1273
+ if (phaseIdx >= 0) {
1274
+ for (let i = phaseIdx; i < def.phases.length; i++) {
1275
+ const pp = record.phaseProgress[def.phases[i].name];
1276
+ if (pp) {
1277
+ pp.status = "pending";
1278
+ pp.startedAt = void 0;
1279
+ pp.completedAt = void 0;
1280
+ pp.error = void 0;
1281
+ }
1282
+ }
1283
+ }
1284
+ }
1177
1285
  record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1178
1286
  this.save();
1179
1287
  logger5.info("Issue reset to phase", { issueIid, phase, state: targetState });
@@ -1186,6 +1294,16 @@ var IssueTracker = class extends BaseTracker {
1186
1294
  const restoreState = record.failedAtState ?? "pending" /* Pending */;
1187
1295
  record.state = restoreState;
1188
1296
  record.lastError = void 0;
1297
+ record.processingLock = void 0;
1298
+ if (record.phaseProgress && record.currentPhase) {
1299
+ const pp = record.phaseProgress[record.currentPhase];
1300
+ if (pp && pp.status === "failed") {
1301
+ pp.status = "pending";
1302
+ pp.startedAt = void 0;
1303
+ pp.completedAt = void 0;
1304
+ pp.error = void 0;
1305
+ }
1306
+ }
1189
1307
  record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1190
1308
  this.save();
1191
1309
  logger5.info("Issue reset for retry", { issueIid, restoreState });
@@ -1217,6 +1335,7 @@ var IssueTracker = class extends BaseTracker {
1217
1335
  record.lastError = "Interrupted by service restart";
1218
1336
  record.lastErrorRetryable = false;
1219
1337
  record.attempts += 1;
1338
+ record.processingLock = void 0;
1220
1339
  record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1221
1340
  count++;
1222
1341
  eventBus.emitTyped("issue:failed", {
@@ -1233,6 +1352,7 @@ var IssueTracker = class extends BaseTracker {
1233
1352
  attempts: record.attempts
1234
1353
  });
1235
1354
  record.lastErrorRetryable = false;
1355
+ record.processingLock = void 0;
1236
1356
  record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1237
1357
  count++;
1238
1358
  }
@@ -1408,7 +1528,14 @@ var PlanPersistence = class _PlanPersistence {
1408
1528
  }
1409
1529
  updatePhaseProgress(phaseName, status, error, options) {
1410
1530
  const progress = this.readProgress();
1411
- if (!progress) return;
1531
+ if (!progress) {
1532
+ logger6.warn("Cannot update phase progress: progress.json not found", {
1533
+ issueIid: this.issueIid,
1534
+ phase: phaseName,
1535
+ targetStatus: status
1536
+ });
1537
+ return;
1538
+ }
1412
1539
  const now = (/* @__PURE__ */ new Date()).toISOString();
1413
1540
  if (!progress.phases[phaseName]) {
1414
1541
  progress.phases[phaseName] = { status: "pending" };
@@ -1438,10 +1565,37 @@ var PlanPersistence = class _PlanPersistence {
1438
1565
  const progress = this.readProgress();
1439
1566
  return progress?.phases[phaseName]?.sessionId;
1440
1567
  }
1568
+ // ---------------------------------------------------------------------------
1569
+ // 通用文件操作 — 替代阶段中散落的 fs 直接调用
1570
+ // ---------------------------------------------------------------------------
1571
+ /** 读取 planDir 下指定文件,不存在返回 null */
1572
+ readFile(filename) {
1573
+ const filePath = path2.join(this.planDir, filename);
1574
+ if (!fs2.existsSync(filePath)) return null;
1575
+ try {
1576
+ return fs2.readFileSync(filePath, "utf-8");
1577
+ } catch {
1578
+ return null;
1579
+ }
1580
+ }
1581
+ /** 检查 planDir 下指定文件是否就绪(存在 + 大于 minBytes) */
1582
+ isArtifactReady(filename, minBytes = 50) {
1583
+ const filePath = path2.join(this.planDir, filename);
1584
+ if (!fs2.existsSync(filePath)) return false;
1585
+ try {
1586
+ return fs2.statSync(filePath).size >= minBytes;
1587
+ } catch {
1588
+ return false;
1589
+ }
1590
+ }
1591
+ /** 写入 planDir 下指定文件(自动 ensureDir) */
1592
+ writeFile(filename, content) {
1593
+ this.ensureDir();
1594
+ fs2.writeFileSync(path2.join(this.planDir, filename), content, "utf-8");
1595
+ }
1441
1596
  };
1442
1597
 
1443
1598
  // src/phases/BasePhase.ts
1444
- import fs3 from "fs";
1445
1599
  import path4 from "path";
1446
1600
 
1447
1601
  // src/rules/RuleResolver.ts
@@ -1539,50 +1693,6 @@ ${rule.content}`;
1539
1693
  }
1540
1694
  };
1541
1695
 
1542
- // src/notesync/NoteSyncSettings.ts
1543
- var noteSyncOverride;
1544
- function getNoteSyncEnabled(cfg) {
1545
- return noteSyncOverride ?? cfg.issueNoteSync.enabled;
1546
- }
1547
- function setNoteSyncOverride(value) {
1548
- noteSyncOverride = value;
1549
- }
1550
- function isNoteSyncEnabledForIssue(issueIid, tracker, cfg) {
1551
- const record = tracker.get(issueIid);
1552
- if (record?.issueNoteSyncEnabled !== void 0) return record.issueNoteSyncEnabled;
1553
- return getNoteSyncEnabled(cfg);
1554
- }
1555
- var SUMMARY_MAX_LENGTH = 500;
1556
- function truncateToSummary(content) {
1557
- if (content.length <= SUMMARY_MAX_LENGTH) return content;
1558
- const cut = content.slice(0, SUMMARY_MAX_LENGTH);
1559
- const lastNewline = cut.lastIndexOf("\n\n");
1560
- const boundary = lastNewline > SUMMARY_MAX_LENGTH * 0.3 ? lastNewline : cut.lastIndexOf("\n");
1561
- const summary = boundary > SUMMARY_MAX_LENGTH * 0.3 ? cut.slice(0, boundary) : cut;
1562
- return summary + "\n\n...";
1563
- }
1564
- function buildNoteSyncComment(phaseName, phaseLabel, docUrl, dashboardUrl, summary) {
1565
- const emoji = {
1566
- analysis: "\u{1F50D}",
1567
- design: "\u{1F4D0}",
1568
- implement: "\u{1F4BB}",
1569
- verify: "\u2705",
1570
- plan: "\u{1F4CB}",
1571
- review: "\u{1F440}",
1572
- build: "\u{1F528}"
1573
- };
1574
- const icon = emoji[phaseName] || "\u{1F4CB}";
1575
- return [
1576
- t("notesync.phaseCompleted", { icon, label: phaseLabel }),
1577
- "",
1578
- summary,
1579
- "",
1580
- "---",
1581
- t("notesync.viewDoc", { label: phaseLabel, url: docUrl }),
1582
- t("notesync.viewDashboard", { url: dashboardUrl })
1583
- ].join("\n");
1584
- }
1585
-
1586
1696
  // src/phases/BasePhase.ts
1587
1697
  var BasePhase = class _BasePhase {
1588
1698
  static MIN_ARTIFACT_BYTES = 50;
@@ -1591,27 +1701,13 @@ var BasePhase = class _BasePhase {
1591
1701
  plan;
1592
1702
  config;
1593
1703
  logger;
1594
- lifecycle;
1595
- hooks;
1596
1704
  /** 多仓模式下所有 repo 的 GitOperations(repo name → GitOperations) */
1597
1705
  wtGitMap;
1598
- get startState() {
1599
- const states = this.lifecycle.getPhaseStates(this.phaseName);
1600
- if (!states) throw new Error(`Phase "${this.phaseName}" not found in lifecycle manager`);
1601
- return states.startState;
1602
- }
1603
- get doneState() {
1604
- const states = this.lifecycle.getPhaseStates(this.phaseName);
1605
- if (!states) throw new Error(`Phase "${this.phaseName}" not found in lifecycle manager`);
1606
- return states.doneState;
1607
- }
1608
- constructor(aiRunner, git, plan, config, lifecycle, hooks) {
1706
+ constructor(aiRunner, git, plan, config) {
1609
1707
  this.aiRunner = aiRunner;
1610
1708
  this.git = git;
1611
1709
  this.plan = plan;
1612
1710
  this.config = config;
1613
- this.lifecycle = lifecycle;
1614
- this.hooks = hooks;
1615
1711
  this.logger = logger.child(this.constructor.name);
1616
1712
  }
1617
1713
  /** 注入多仓库 GitOperations map(由编排器在创建 phase 后调用) */
@@ -1621,40 +1717,29 @@ var BasePhase = class _BasePhase {
1621
1717
  getRunMode() {
1622
1718
  return void 0;
1623
1719
  }
1720
+ /** 获取阶段预期的产物文件列表。编排器需通过此方法同步产物到 Issue。 */
1624
1721
  getResultFiles(_ctx) {
1625
1722
  return [];
1626
1723
  }
1627
- async execute(ctx) {
1724
+ /**
1725
+ * 纯逻辑执行 — 零副作用,返回 PhaseOutcome。
1726
+ *
1727
+ * 阶段内部仅做:构建 prompt → AI 执行 → 验证产物 → 返回结果。
1728
+ * 所有副作用(状态转换、Git commit、Issue 评论、事件推送)由编排器统一处理。
1729
+ */
1730
+ async run(ctx, callbacks) {
1628
1731
  const displayId = Number(ctx.demand.sourceRef.displayId);
1629
- this.logger.info(`Phase ${this.phaseName} starting`, { issueIid: displayId });
1630
- const resumeInfo = this.resolveResumeInfo();
1631
- await this.notifyPhaseStart();
1632
- this.plan.updatePhaseProgress(this.phaseName, "in_progress", void 0, {
1633
- preserveSessionId: resumeInfo.resumable
1634
- });
1635
- await this.notifyComment(
1636
- issueProgressComment(this.phaseName, "in_progress")
1637
- );
1638
- const phaseLabel = t(`phase.${this.phaseName}`) || this.phaseName;
1639
- const systemMessage = resumeInfo.resumable ? t("basePhase.aiResuming", { label: phaseLabel }) : t("basePhase.aiStarting", { label: phaseLabel });
1640
- eventBus.emitTyped("agent:output", {
1641
- issueIid: displayId,
1642
- phase: this.phaseName,
1643
- event: { type: "system", content: systemMessage, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
1644
- });
1645
- let basePrompt = this.buildPrompt(ctx);
1732
+ const expectedResultFiles = this.getResultFiles(ctx);
1733
+ let prompt = this.buildPrompt(ctx);
1646
1734
  const wsSection = buildWorkspaceSection(ctx.workspace);
1647
- if (wsSection) {
1648
- basePrompt = `${basePrompt}
1735
+ if (wsSection) prompt += `
1649
1736
 
1650
1737
  ${wsSection}`;
1651
- }
1652
- const matchedRulesText = await this.resolveRules(ctx);
1653
- const fullPrompt = matchedRulesText ? `${basePrompt}
1738
+ const rules = await this.resolveRules(ctx);
1739
+ if (rules) prompt += `
1654
1740
 
1655
- ${t("basePhase.rulesSection", { rules: matchedRulesText })}` : basePrompt;
1656
- const resumePrompt = t("basePhase.resumePrompt");
1657
- const onInputRequired = this.buildInputRequiredHandler(displayId);
1741
+ ${t("basePhase.rulesSection", { rules })}`;
1742
+ const resumeInfo = this.resolveResumeInfo();
1658
1743
  let result;
1659
1744
  if (resumeInfo.resumable) {
1660
1745
  this.logger.info("Attempting session resume", {
@@ -1665,37 +1750,53 @@ ${t("basePhase.rulesSection", { rules: matchedRulesText })}` : basePrompt;
1665
1750
  result = await this.runWithResumeFallback(
1666
1751
  displayId,
1667
1752
  resumeInfo.sessionId,
1668
- resumePrompt,
1669
- fullPrompt,
1670
- onInputRequired,
1671
- ctx
1753
+ t("basePhase.resumePrompt"),
1754
+ prompt,
1755
+ callbacks?.onInputRequired,
1756
+ ctx,
1757
+ callbacks?.onStreamEvent
1672
1758
  );
1673
1759
  } else {
1674
- result = await this.runAI(displayId, fullPrompt, onInputRequired, void 0, ctx);
1760
+ result = await this.runAI(
1761
+ displayId,
1762
+ prompt,
1763
+ callbacks?.onInputRequired,
1764
+ void 0,
1765
+ ctx,
1766
+ callbacks?.onStreamEvent
1767
+ );
1675
1768
  }
1676
1769
  if (!result.success) {
1677
1770
  this.persistSessionId(result.sessionId);
1678
- const failReason = (result.errorMessage || result.output).slice(0, 500);
1679
- const shortReason = (result.errorMessage || result.output).slice(0, 200);
1680
- this.plan.updatePhaseProgress(this.phaseName, "failed", failReason);
1681
- await this.notifyPhaseFailed(failReason);
1682
- await this.notifyComment(
1683
- issueProgressComment(this.phaseName, "failed", t("basePhase.error", { message: shortReason }))
1684
- );
1685
- throw new AIExecutionError(this.phaseName, `Phase ${this.phaseName} failed: ${shortReason}`, {
1771
+ return {
1772
+ status: "failed",
1686
1773
  output: result.output,
1774
+ sessionId: result.sessionId,
1687
1775
  exitCode: result.exitCode,
1688
- isRetryable: this.classifyRetryable(result)
1689
- });
1776
+ error: {
1777
+ message: (result.errorMessage || result.output).slice(0, 500),
1778
+ isRetryable: this.classifyRetryable(result),
1779
+ rawOutput: result.output
1780
+ }
1781
+ };
1690
1782
  }
1691
1783
  this.persistSessionId(result.sessionId);
1692
- await this.validatePhaseOutput(ctx, displayId);
1693
- await this.notifyPhaseDone();
1694
- this.plan.updatePhaseProgress(this.phaseName, "completed");
1695
- await this.commitPlanFiles(ctx, displayId);
1696
- await this.syncResultToIssue(ctx, displayId);
1697
- this.logger.info(`Phase ${this.phaseName} completed`, { issueIid: displayId });
1698
- return result;
1784
+ try {
1785
+ await this.validatePhaseOutput(ctx, displayId, expectedResultFiles);
1786
+ } catch (err) {
1787
+ return {
1788
+ status: "failed",
1789
+ output: result.output,
1790
+ sessionId: result.sessionId,
1791
+ error: { message: err.message, isRetryable: false, rawOutput: result.output }
1792
+ };
1793
+ }
1794
+ return {
1795
+ status: "completed",
1796
+ output: result.output,
1797
+ sessionId: result.sessionId,
1798
+ exitCode: result.exitCode
1799
+ };
1699
1800
  }
1700
1801
  // ── Session resume helpers ──
1701
1802
  resolveResumeInfo() {
@@ -1722,7 +1823,20 @@ ${t("basePhase.rulesSection", { rules: matchedRulesText })}` : basePrompt;
1722
1823
  }
1723
1824
  return this.plan.baseDir;
1724
1825
  }
1725
- async runAI(displayId, prompt, onInputRequired, options, ctx) {
1826
+ /** 检查预期产物文件是否已就绪(存在且满足最小字节数),不抛异常 */
1827
+ checkArtifactsReady(ctx, _displayId) {
1828
+ const resultFiles = this.getResultFiles(ctx);
1829
+ if (resultFiles.length === 0) return true;
1830
+ for (const file of resultFiles) {
1831
+ if (!this.plan.isArtifactReady(file.filename, _BasePhase.MIN_ARTIFACT_BYTES)) return false;
1832
+ }
1833
+ return true;
1834
+ }
1835
+ async runAI(displayId, prompt, onInputRequired, options, ctx, onStreamEvent) {
1836
+ const resultFiles = this.getResultFiles(ctx);
1837
+ const snapshotFilenames = resultFiles.map((f) => f.filename);
1838
+ const artifactCheck = snapshotFilenames.length > 0 ? () => snapshotFilenames.every((fn) => this.plan.isArtifactReady(fn, _BasePhase.MIN_ARTIFACT_BYTES)) : void 0;
1839
+ const artifactPaths = snapshotFilenames.length > 0 ? snapshotFilenames.map((fn) => path4.join(this.plan.planDir, fn)) : void 0;
1726
1840
  let capturedSessionId;
1727
1841
  const result = await this.aiRunner.run({
1728
1842
  prompt,
@@ -1730,6 +1844,9 @@ ${t("basePhase.rulesSection", { rules: matchedRulesText })}` : basePrompt;
1730
1844
  timeoutMs: this.config.ai.phaseTimeoutMs,
1731
1845
  idleTimeoutMs: this.config.ai.idleTimeoutMs,
1732
1846
  mode: this.getRunMode(),
1847
+ phaseName: this.phaseName,
1848
+ artifactCheck,
1849
+ artifactPaths,
1733
1850
  sessionId: options?.sessionId,
1734
1851
  continueSession: options?.continueSession,
1735
1852
  onStreamEvent: (event) => {
@@ -1740,11 +1857,7 @@ ${t("basePhase.rulesSection", { rules: matchedRulesText })}` : basePrompt;
1740
1857
  this.persistSessionId(capturedSessionId);
1741
1858
  }
1742
1859
  }
1743
- eventBus.emitTyped("agent:output", {
1744
- issueIid: displayId,
1745
- phase: this.phaseName,
1746
- event
1747
- });
1860
+ onStreamEvent?.(event);
1748
1861
  },
1749
1862
  onInputRequired
1750
1863
  });
@@ -1753,27 +1866,23 @@ ${t("basePhase.rulesSection", { rules: matchedRulesText })}` : basePrompt;
1753
1866
  }
1754
1867
  return result;
1755
1868
  }
1756
- async runWithResumeFallback(displayId, sessionId, resumePrompt, fullPrompt, onInputRequired, ctx) {
1869
+ async runWithResumeFallback(displayId, sessionId, resumePrompt, fullPrompt, onInputRequired, ctx, onStreamEvent) {
1757
1870
  const result = await this.runAI(displayId, resumePrompt, onInputRequired, {
1758
1871
  sessionId,
1759
1872
  continueSession: true
1760
- }, ctx);
1873
+ }, ctx, onStreamEvent);
1761
1874
  if (!result.success && this.isResumeFailure(result)) {
1762
1875
  this.logger.warn(t("basePhase.resumeFallback"), {
1763
1876
  issueIid: displayId,
1764
1877
  phase: this.phaseName,
1765
1878
  exitCode: result.exitCode
1766
1879
  });
1767
- eventBus.emitTyped("agent:output", {
1768
- issueIid: displayId,
1769
- phase: this.phaseName,
1770
- event: {
1771
- type: "system",
1772
- content: t("basePhase.resumeFallback"),
1773
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1774
- }
1880
+ onStreamEvent?.({
1881
+ type: "system",
1882
+ content: t("basePhase.resumeFallback"),
1883
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1775
1884
  });
1776
- return this.runAI(displayId, fullPrompt, onInputRequired, void 0, ctx);
1885
+ return this.runAI(displayId, fullPrompt, onInputRequired, void 0, ctx, onStreamEvent);
1777
1886
  }
1778
1887
  return result;
1779
1888
  }
@@ -1814,63 +1923,6 @@ ${t("basePhase.rulesSection", { rules: matchedRulesText })}` : basePrompt;
1814
1923
  this.plan.updatePhaseSessionId(this.phaseName, sessionId);
1815
1924
  }
1816
1925
  }
1817
- // ── Hook dispatch methods ──
1818
- async notifyPhaseStart() {
1819
- await this.hooks.onPhaseStart(this.phaseName);
1820
- }
1821
- async notifyPhaseDone() {
1822
- await this.hooks.onPhaseDone(this.phaseName);
1823
- }
1824
- async notifyPhaseFailed(error) {
1825
- await this.hooks.onPhaseFailed(this.phaseName, error);
1826
- }
1827
- async notifyComment(message) {
1828
- try {
1829
- await this.hooks.onComment(message);
1830
- } catch (err) {
1831
- this.logger.warn("Hook onComment failed", { error: err.message });
1832
- }
1833
- }
1834
- /**
1835
- * Build an onInputRequired handler for ACP permission delegation.
1836
- * Only returns a handler when codebuddyAcpAutoApprove is false,
1837
- * enabling review gate delegation for permission requests.
1838
- */
1839
- buildInputRequiredHandler(displayId) {
1840
- if (this.config.ai.codebuddyAcpAutoApprove !== false) return void 0;
1841
- return (request) => {
1842
- if (request.type !== "plan-approval") {
1843
- return Promise.resolve("allow");
1844
- }
1845
- this.logger.info("ACP plan-approval requested, delegating to review gate", {
1846
- issueIid: displayId,
1847
- phase: this.phaseName
1848
- });
1849
- eventBus.emitTyped("review:requested", { issueIid: displayId });
1850
- return new Promise((resolve) => {
1851
- const onApproved = (payload) => {
1852
- const data = payload.data;
1853
- if (data.issueIid !== displayId) return;
1854
- cleanup();
1855
- this.logger.info("ACP plan-approval approved via review gate", { issueIid: displayId });
1856
- resolve("allow");
1857
- };
1858
- const onRejected = (payload) => {
1859
- const data = payload.data;
1860
- if (data.issueIid !== displayId) return;
1861
- cleanup();
1862
- this.logger.info("ACP plan-approval rejected via review gate", { issueIid: displayId });
1863
- resolve("reject");
1864
- };
1865
- const cleanup = () => {
1866
- eventBus.removeListener("review:approved", onApproved);
1867
- eventBus.removeListener("review:rejected", onRejected);
1868
- };
1869
- eventBus.on("review:approved", onApproved);
1870
- eventBus.on("review:rejected", onRejected);
1871
- });
1872
- };
1873
- }
1874
1926
  async resolveRules(ctx) {
1875
1927
  try {
1876
1928
  const knowledge = getProjectKnowledge();
@@ -1900,107 +1952,27 @@ ${t("basePhase.rulesSection", { rules: matchedRulesText })}` : basePrompt;
1900
1952
  }
1901
1953
  return null;
1902
1954
  }
1903
- async syncResultToIssue(ctx, displayId) {
1904
- try {
1905
- const enabled = this.hooks.isNoteSyncEnabled();
1906
- const resultFiles = this.getResultFiles(ctx);
1907
- if (!enabled || resultFiles.length === 0) {
1908
- await this.notifyComment(
1909
- issueProgressComment(this.phaseName, "completed")
1910
- );
1911
- return;
1912
- }
1913
- const baseUrl = this.config.issueNoteSync.webBaseUrl.replace(/\/$/, "");
1914
- const phaseLabel = t(`phase.${this.phaseName}`) || this.phaseName;
1915
- const dashboardUrl = `${baseUrl}/?issue=${displayId}`;
1916
- for (const file of resultFiles) {
1917
- const content = this.readResultFile(displayId, file.filename);
1918
- if (!content) continue;
1919
- const summary = truncateToSummary(content);
1920
- const docUrl = `${baseUrl}/doc/${displayId}/${file.filename}`;
1921
- const comment = buildNoteSyncComment(
1922
- this.phaseName,
1923
- file.label || phaseLabel,
1924
- docUrl,
1925
- dashboardUrl,
1926
- summary
1927
- );
1928
- await this.notifyComment(comment);
1929
- this.logger.info("Result synced to issue", { issueIid: displayId, file: file.filename });
1930
- }
1931
- } catch (err) {
1932
- this.logger.warn("Failed to sync result to issue", { error: err.message });
1933
- await this.notifyComment(
1934
- issueProgressComment(this.phaseName, "completed")
1935
- );
1936
- }
1937
- }
1938
- readResultFile(issueIid, filename) {
1939
- const planDir = path4.join(this.plan.baseDir, ".claude-plan", `issue-${issueIid}`);
1940
- const filePath = path4.join(planDir, filename);
1941
- if (!fs3.existsSync(filePath)) return null;
1942
- try {
1943
- return fs3.readFileSync(filePath, "utf-8");
1944
- } catch {
1945
- return null;
1946
- }
1947
- }
1948
- async validatePhaseOutput(ctx, displayId) {
1949
- const resultFiles = this.getResultFiles(ctx);
1955
+ async validatePhaseOutput(ctx, _displayId, resultFiles = this.getResultFiles(ctx)) {
1950
1956
  if (resultFiles.length === 0) return;
1951
- const planDir = path4.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
1952
1957
  const missing = [];
1953
1958
  for (const file of resultFiles) {
1954
- const filePath = path4.join(planDir, file.filename);
1955
- if (!fs3.existsSync(filePath)) {
1956
- missing.push(file.filename);
1957
- continue;
1958
- }
1959
- const stat = fs3.statSync(filePath);
1960
- if (stat.size < _BasePhase.MIN_ARTIFACT_BYTES) {
1961
- missing.push(`${file.filename} (${stat.size} bytes, \u5185\u5BB9\u4E0D\u8DB3)`);
1959
+ if (!this.plan.isArtifactReady(file.filename, _BasePhase.MIN_ARTIFACT_BYTES)) {
1960
+ const content = this.plan.readFile(file.filename);
1961
+ if (content === null) {
1962
+ missing.push(file.filename);
1963
+ } else {
1964
+ missing.push(`${file.filename} (${Buffer.byteLength(content, "utf-8")} bytes, \u5185\u5BB9\u4E0D\u8DB3)`);
1965
+ }
1962
1966
  }
1963
1967
  }
1964
1968
  if (missing.length > 0) {
1965
1969
  const msg = `AI \u8FDB\u7A0B\u6210\u529F\u9000\u51FA\u4F46\u672A\u751F\u6210\u9884\u671F\u4EA7\u7269: ${missing.join(", ")}`;
1966
- this.logger.error(msg, { phase: this.phaseName, displayId });
1970
+ this.logger.error(msg, { phase: this.phaseName, displayId: _displayId });
1967
1971
  throw new AIExecutionError(this.phaseName, msg, { output: "", exitCode: 0 });
1968
1972
  }
1969
1973
  }
1970
- async commitPlanFiles(ctx, displayId) {
1971
- const commitMsg = `chore(auto): ${this.phaseName} phase completed for issue #${displayId}`;
1972
- if (ctx.workspace && ctx.workspace.repos.length > 1) {
1973
- for (const repo of ctx.workspace.repos) {
1974
- const repoGit = this.wtGitMap?.get(repo.name) ?? new GitOperations(repo.gitRootDir);
1975
- const branch = repo.branchPrefix ? `${repo.branchPrefix}-${displayId}` : ctx.branchName;
1976
- try {
1977
- if (await repoGit.hasChanges()) {
1978
- await repoGit.add(["."]);
1979
- await repoGit.commit(commitMsg);
1980
- await repoGit.push(branch);
1981
- this.logger.info("Committed changes for repo", { repo: repo.name, branch });
1982
- }
1983
- } catch (err) {
1984
- this.logger.warn("Failed to commit/push for repo", {
1985
- repo: repo.name,
1986
- error: err.message
1987
- });
1988
- }
1989
- }
1990
- } else {
1991
- if (await this.git.hasChanges()) {
1992
- await this.git.add(["."]);
1993
- await this.git.commit(commitMsg);
1994
- await this.git.push(ctx.branchName);
1995
- }
1996
- }
1997
- }
1998
1974
  };
1999
1975
 
2000
- // src/phases/VerifyPhase.ts
2001
- import fs4 from "fs";
2002
- import path5 from "path";
2003
-
2004
1976
  // src/verify/VerifyReportParser.ts
2005
1977
  var LINT_FAIL_RE = /\*{0,2}Lint\s*(?:结果|Result)\*{0,2}\s*[::]\s*(?:失败|failed|fail|未通过)/i;
2006
1978
  var BUILD_FAIL_RE = /\*{0,2}Build\s*(?:结果|Result)\*{0,2}\s*[::]\s*(?:失败|failed|fail|未通过)/i;
@@ -2083,41 +2055,40 @@ var VerifyPhase = class extends BasePhase {
2083
2055
  const filename = ctx?.pipelineMode === "plan-mode" ? "02-verify-report.md" : "04-verify-report.md";
2084
2056
  return [{ filename, label: "\u9A8C\u8BC1\u62A5\u544A" }];
2085
2057
  }
2086
- async execute(ctx) {
2087
- const result = await super.execute(ctx);
2088
- if (result.success) {
2089
- const report = this.readVerifyReport(ctx);
2090
- if (report) {
2091
- const parsed = this.reportParser.parse(report);
2092
- if (this.config.verifyFixLoop.todolistCheckEnabled && !parsed.todolistStats) {
2093
- const planContent = this.readPlanFile(ctx);
2094
- if (planContent) {
2095
- const todoStats = this.reportParser.parseTodolistFromPlan(planContent);
2096
- if (todoStats.total > 0) {
2097
- parsed.todolistStats = todoStats;
2098
- parsed.todolistComplete = todoStats.completed === todoStats.total;
2099
- if (!parsed.todolistComplete) {
2100
- parsed.failureReasons.push(
2101
- `Todolist \u672A\u5168\u90E8\u5B8C\u6210(${todoStats.completed}/${todoStats.total})`
2102
- );
2103
- parsed.passed = false;
2104
- }
2058
+ async run(ctx, callbacks) {
2059
+ const outcome = await super.run(ctx, callbacks);
2060
+ if (outcome.status !== "completed") return outcome;
2061
+ const report = this.readVerifyReport(ctx);
2062
+ if (report) {
2063
+ const parsed = this.reportParser.parse(report);
2064
+ if (this.config.verifyFixLoop.todolistCheckEnabled && !parsed.todolistStats) {
2065
+ const planContent = this.readPlanFile();
2066
+ if (planContent) {
2067
+ const todoStats = this.reportParser.parseTodolistFromPlan(planContent);
2068
+ if (todoStats.total > 0) {
2069
+ parsed.todolistStats = todoStats;
2070
+ parsed.todolistComplete = todoStats.completed === todoStats.total;
2071
+ if (!parsed.todolistComplete) {
2072
+ parsed.failureReasons.push(
2073
+ `Todolist \u672A\u5168\u90E8\u5B8C\u6210(${todoStats.completed}/${todoStats.total})`
2074
+ );
2075
+ parsed.passed = false;
2105
2076
  }
2106
2077
  }
2107
2078
  }
2108
- this.logger.info("Verify report parsed", {
2109
- passed: parsed.passed,
2110
- lintPassed: parsed.lintPassed,
2111
- buildPassed: parsed.buildPassed,
2112
- testPassed: parsed.testPassed,
2113
- todolistComplete: parsed.todolistComplete,
2114
- todolistStats: parsed.todolistStats,
2115
- failureCount: parsed.failureReasons.length
2116
- });
2117
- return { ...result, verifyReport: parsed };
2118
2079
  }
2080
+ this.logger.info("Verify report parsed", {
2081
+ passed: parsed.passed,
2082
+ lintPassed: parsed.lintPassed,
2083
+ buildPassed: parsed.buildPassed,
2084
+ testPassed: parsed.testPassed,
2085
+ todolistComplete: parsed.todolistComplete,
2086
+ todolistStats: parsed.todolistStats,
2087
+ failureCount: parsed.failureReasons.length
2088
+ });
2089
+ return { ...outcome, data: { ...outcome.data, verifyReport: parsed } };
2119
2090
  }
2120
- return result;
2091
+ return outcome;
2121
2092
  }
2122
2093
  buildPrompt(ctx) {
2123
2094
  const pc = demandToPromptContext(ctx.demand);
@@ -2132,26 +2103,10 @@ var VerifyPhase = class extends BasePhase {
2132
2103
  readVerifyReport(ctx) {
2133
2104
  const files = this.getResultFiles(ctx);
2134
2105
  if (files.length === 0) return null;
2135
- const displayId = Number(ctx.demand.sourceRef.displayId);
2136
- const planDir = path5.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
2137
- const filePath = path5.join(planDir, files[0].filename);
2138
- if (!fs4.existsSync(filePath)) return null;
2139
- try {
2140
- return fs4.readFileSync(filePath, "utf-8");
2141
- } catch {
2142
- return null;
2143
- }
2106
+ return this.plan.readFile(files[0].filename);
2144
2107
  }
2145
- readPlanFile(ctx) {
2146
- const displayId = Number(ctx.demand.sourceRef.displayId);
2147
- const planDir = path5.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
2148
- const filePath = path5.join(planDir, "01-plan.md");
2149
- if (!fs4.existsSync(filePath)) return null;
2150
- try {
2151
- return fs4.readFileSync(filePath, "utf-8");
2152
- } catch {
2153
- return null;
2154
- }
2108
+ readPlanFile() {
2109
+ return this.plan.readFile("01-plan.md");
2155
2110
  }
2156
2111
  };
2157
2112
 
@@ -2197,7 +2152,11 @@ var BuildPhase = class extends BasePhase {
2197
2152
  let hasAnyChanges = false;
2198
2153
  if (ctx.workspace && ctx.workspace.repos.length > 1) {
2199
2154
  for (const repo of ctx.workspace.repos) {
2200
- const repoGit = new GitOperations(repo.gitRootDir);
2155
+ const repoGit = this.wtGitMap?.get(repo.name);
2156
+ if (!repoGit) {
2157
+ this.logger.warn("Missing GitOperations for repo in validation", { repo: repo.name });
2158
+ continue;
2159
+ }
2201
2160
  if (await repoGit.hasChanges()) {
2202
2161
  hasAnyChanges = true;
2203
2162
  break;
@@ -2231,13 +2190,9 @@ var BuildPhase = class extends BasePhase {
2231
2190
  }
2232
2191
  };
2233
2192
 
2234
- // src/phases/ReleasePhase.ts
2235
- import fs6 from "fs";
2236
- import path7 from "path";
2237
-
2238
2193
  // src/release/ReleaseDetectCache.ts
2239
- import fs5 from "fs";
2240
- import path6 from "path";
2194
+ import fs3 from "fs";
2195
+ import path5 from "path";
2241
2196
  import { createHash } from "crypto";
2242
2197
  var logger7 = logger.child("ReleaseDetectCache");
2243
2198
  function hashProjectPath(projectPath) {
@@ -2246,10 +2201,10 @@ function hashProjectPath(projectPath) {
2246
2201
  var ReleaseDetectCache = class {
2247
2202
  cacheDir;
2248
2203
  constructor(dataDir) {
2249
- this.cacheDir = path6.join(dataDir, "release-detect");
2204
+ this.cacheDir = path5.join(dataDir, "release-detect");
2250
2205
  }
2251
2206
  filePath(projectPath) {
2252
- return path6.join(this.cacheDir, `${hashProjectPath(projectPath)}.json`);
2207
+ return path5.join(this.cacheDir, `${hashProjectPath(projectPath)}.json`);
2253
2208
  }
2254
2209
  /**
2255
2210
  * 读取缓存。返回 null 如果不存在、已过期或校验失败。
@@ -2257,8 +2212,8 @@ var ReleaseDetectCache = class {
2257
2212
  get(projectPath, ttlMs) {
2258
2213
  const fp = this.filePath(projectPath);
2259
2214
  try {
2260
- if (!fs5.existsSync(fp)) return null;
2261
- const raw = fs5.readFileSync(fp, "utf-8");
2215
+ if (!fs3.existsSync(fp)) return null;
2216
+ const raw = fs3.readFileSync(fp, "utf-8");
2262
2217
  const data = JSON.parse(raw);
2263
2218
  if (data.projectPath !== projectPath) {
2264
2219
  logger7.warn("Cache projectPath mismatch, ignoring", { expected: projectPath, got: data.projectPath });
@@ -2281,10 +2236,10 @@ var ReleaseDetectCache = class {
2281
2236
  set(result) {
2282
2237
  const fp = this.filePath(result.projectPath);
2283
2238
  try {
2284
- if (!fs5.existsSync(this.cacheDir)) {
2285
- fs5.mkdirSync(this.cacheDir, { recursive: true });
2239
+ if (!fs3.existsSync(this.cacheDir)) {
2240
+ fs3.mkdirSync(this.cacheDir, { recursive: true });
2286
2241
  }
2287
- fs5.writeFileSync(fp, JSON.stringify(result, null, 2), "utf-8");
2242
+ fs3.writeFileSync(fp, JSON.stringify(result, null, 2), "utf-8");
2288
2243
  logger7.debug("Release detect cache written", { projectPath: result.projectPath, path: fp });
2289
2244
  } catch (err) {
2290
2245
  logger7.warn("Failed to write release detect cache", { path: fp, error: err.message });
@@ -2296,8 +2251,8 @@ var ReleaseDetectCache = class {
2296
2251
  invalidate(projectPath) {
2297
2252
  const fp = this.filePath(projectPath);
2298
2253
  try {
2299
- if (fs5.existsSync(fp)) {
2300
- fs5.unlinkSync(fp);
2254
+ if (fs3.existsSync(fp)) {
2255
+ fs3.unlinkSync(fp);
2301
2256
  logger7.info("Release detect cache invalidated", { projectPath });
2302
2257
  return true;
2303
2258
  }
@@ -2312,54 +2267,66 @@ var ReleaseDetectCache = class {
2312
2267
  // src/prompts/release-templates.ts
2313
2268
  function releaseDetectPrompt(ctx) {
2314
2269
  const pd = `.claude-plan/issue-${ctx.issueIid}`;
2315
- return `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684 DevOps \u5206\u6790\u5E08\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u63A2\u7D22\u5F53\u524D\u9879\u76EE\uFF0C\u67E5\u627E\u6240\u6709\u4E0E**\u53D1\u5E03/\u90E8\u7F72**\u76F8\u5173\u7684\u673A\u5236\u3002
2270
+ return `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684 DevOps \u5206\u6790\u5E08\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u63A2\u7D22\u5F53\u524D\u9879\u76EE\uFF0C\u5224\u65AD\u662F\u5426\u5B58\u5728 **AI \u53EF\u4EE5\u5728\u672C\u5730\u76F4\u63A5\u6267\u884C\u7684\u53D1\u5E03\u6D41\u7A0B**\u3002
2316
2271
 
2317
- ## \u63A2\u7D22\u8303\u56F4
2272
+ ## \u5173\u952E\u5224\u5B9A\u539F\u5219
2318
2273
 
2319
- \u8BF7\u6309\u4F18\u5148\u7EA7\u4F9D\u6B21\u68C0\u67E5\u4EE5\u4E0B\u4F4D\u7F6E\uFF1A
2274
+ \`hasReleaseCapability\` \u4EC5\u5728\u4EE5\u4E0B\u60C5\u51B5\u4E3A \`true\`\uFF1A
2275
+ - \u5B58\u5728 **AI \u53EF\u4EE5\u5728\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u76F4\u63A5\u8FD0\u884C** \u7684\u53D1\u5E03\u547D\u4EE4\u6216\u811A\u672C
2276
+ - \u4F8B\u5982\uFF1A\`npm publish\`\u3001\`make release\`\u3001\`scripts/release.sh\`\u3001\u5E26\u660E\u786E\u6B65\u9AA4\u7684 Skill \u6587\u4EF6
2320
2277
 
2321
- 1. **Skill \u6587\u4EF6**
2322
- - \`.cursor/skills/\` \u76EE\u5F55\u4E0B\u4E0E release/deploy/publish \u76F8\u5173\u7684 skill
2323
- - \u9879\u76EE\u6839\u76EE\u5F55\u7684 \`SKILL.md\` \u6216 \`release.md\`
2278
+ \u4EE5\u4E0B\u60C5\u51B5\u5FC5\u987B\u8BBE\u4E3A \`false\`\uFF1A
2279
+ - **\u4EC5\u6709 Dockerfile** \u2014 Docker \u955C\u50CF\u7531\u5916\u90E8 CI/CD \u7CFB\u7EDF\uFF08\u84DD\u76FE/Landun/Jenkins/GitHub Actions\uFF09\u6784\u5EFA\u548C\u63A8\u9001\uFF0CAI \u65E0\u6CD5\u66FF\u4EE3
2280
+ - **\u4EC5\u6709\u5916\u90E8 CI/CD \u914D\u7F6E** \u2014 \`.gitlab-ci.yml\` \u7B49\u5B9A\u4E49\u4E86\u6D41\u6C34\u7EBF\uFF0C\u4F46\u9700\u8981 CI \u73AF\u5883\u6267\u884C\uFF0CAI \u65E0\u6CD5\u89E6\u53D1
2281
+ - **\u9879\u76EE\u662F private \u5305\u4E14\u4E0D\u53D1\u5E03\u5230 registry** \u2014 \`package.json\` \u4E2D \`"private": true\` \u4E14\u65E0 publish \u811A\u672C
2282
+ - **\u53D1\u5E03\u76F8\u5173\u4EE3\u7801\u5C5E\u4E8E\u4E1A\u52A1\u57DF** \u2014 \u9879\u76EE\u672C\u8EAB\u662F\u90E8\u7F72\u5E73\u53F0/\u7F16\u6392\u7CFB\u7EDF\uFF0C\u5176\u4EE3\u7801\u662F\u4E3A\u5176\u4ED6\u670D\u52A1\u7F16\u6392\u53D1\u5E03\uFF0C\u4E0D\u662F\u672C\u9879\u76EE\u7684\u53D1\u5E03\u6D41\u7A0B
2283
+ - **\u4EC5\u6709\u6784\u5EFA\u811A\u672C\u65E0\u53D1\u5E03\u6B65\u9AA4** \u2014 \`build.sh\` \u7B49\u53EA\u6784\u5EFA\u4EA7\u7269\u4F46\u4E0D\u5B9E\u9645\u53D1\u5E03
2324
2284
 
2325
- 2. **Rule \u6587\u4EF6**
2326
- - \`.cursor/rules/\` \u76EE\u5F55\u4E0B\u4E0E release/deploy \u76F8\u5173\u7684 \`.mdc\` \u89C4\u5219
2285
+ ## \u63A2\u7D22\u8303\u56F4
2327
2286
 
2328
- 3. **CI/CD \u914D\u7F6E**
2329
- - \`.gitlab-ci.yml\` / \`Jenkinsfile\` / \`.github/workflows/\`
2330
- - \u67E5\u627E release/deploy/publish \u76F8\u5173\u7684 stage/job/workflow
2287
+ \u8BF7\u6309\u4F18\u5148\u7EA7\u4F9D\u6B21\u68C0\u67E5\uFF1A
2331
2288
 
2332
- 4. **Package Manager**
2333
- - \`package.json\` \u4E2D\u7684 \`scripts\` \u5B57\u6BB5\uFF1Apublish\u3001release\u3001deploy \u7B49
2334
- - \`Makefile\` \u4E2D\u7684 release/deploy target
2289
+ 1. **Skill / Rule \u6587\u4EF6**\uFF08\u6700\u9AD8\u4F18\u5148\u7EA7\uFF09
2290
+ - \`.cursor/skills/\` \u4E0B\u4E0E release/deploy/publish \u76F8\u5173\u7684 skill
2291
+ - \`.cursor/rules/\` \u4E0B\u4E0E release/deploy \u76F8\u5173\u7684 \`.mdc\` \u89C4\u5219
2292
+ - \u26A0\uFE0F \u6CE8\u610F\u533A\u5206\uFF1A\u4E3A\u9879\u76EE\u81EA\u8EAB\u53D1\u5E03\u7684 skill vs \u4E3A\u4E1A\u52A1\u57DF\uFF08\u5982"\u6DFB\u52A0\u5236\u54C1\u7C7B\u578B"\uFF09\u7684 skill
2293
+
2294
+ 2. **Package Manager \u53D1\u5E03**
2295
+ - \`package.json\` \u7684 \`scripts\` \u4E2D\u662F\u5426\u6709 \`publish\`\u3001\`release\` \u547D\u4EE4
2296
+ - \`package.json\` \u662F\u5426\u4E3A \`"private": true\`\uFF08\u79C1\u6709\u5305\u901A\u5E38\u4E0D\u53D1\u5E03\uFF09
2335
2297
 
2336
- 5. **\u811A\u672C\u4E0E\u6587\u6863**
2298
+ 3. **\u53D1\u5E03\u811A\u672C**
2337
2299
  - \`scripts/release.*\`\u3001\`scripts/deploy.*\`\u3001\`scripts/publish.*\`
2338
- - \`RELEASE.md\`\u3001\`CHANGELOG.md\`\u3001\`CONTRIBUTING.md\` \u4E2D\u7684\u53D1\u5E03\u8BF4\u660E
2300
+ - \`Makefile\` \u4E2D\u7684 release/deploy target
2301
+
2302
+ 4. **CI/CD \u914D\u7F6E**\uFF08\u4EC5\u8BB0\u5F55\uFF0C\u901A\u5E38\u4E0D\u7B97 AI \u53EF\u6267\u884C\uFF09
2303
+ - \`.gitlab-ci.yml\` / \`Jenkinsfile\` / \`.github/workflows/\`
2304
+
2305
+ 5. **\u5BB9\u5668\u5316**\uFF08\u4EC5\u8BB0\u5F55\uFF0C\u901A\u5E38\u4E0D\u7B97 AI \u53EF\u6267\u884C\uFF09
2339
2306
  - \`Dockerfile\`\u3001\`docker-compose*.yml\`
2340
2307
 
2341
2308
  ## \u8F93\u51FA\u8981\u6C42
2342
2309
 
2343
- \u5C06\u68C0\u6D4B\u7ED3\u679C\u4EE5 **\u4E25\u683C JSON** \u683C\u5F0F\u5199\u5165 \`${pd}/03-release-detect.json\`\uFF1A
2310
+ \u5C06\u68C0\u6D4B\u7ED3\u679C\u4EE5 **\u4E25\u683C JSON** \u683C\u5F0F\u5199\u5165 \`${pd}/05-release-detect.json\`\uFF1A
2344
2311
 
2345
2312
  \`\`\`json
2346
2313
  {
2347
- "hasReleaseCapability": true,
2348
- "releaseMethod": "npm-publish",
2349
- "artifacts": ["package.json", ".gitlab-ci.yml"],
2350
- "instructions": "\u8FD0\u884C npm publish \u53D1\u5E03\u5230 npm registry..."
2314
+ "hasReleaseCapability": false,
2315
+ "releaseMethod": "ci-pipeline",
2316
+ "artifacts": ["Dockerfile", ".gitlab-ci.yml"],
2317
+ "instructions": "\u9879\u76EE\u901A\u8FC7\u5916\u90E8 CI/CD \u6D41\u6C34\u7EBF\u6784\u5EFA Docker \u955C\u50CF\u5E76\u90E8\u7F72\uFF0CAI \u65E0\u6CD5\u76F4\u63A5\u6267\u884C\u53D1\u5E03\u3002"
2351
2318
  }
2352
2319
  \`\`\`
2353
2320
 
2354
2321
  \u5B57\u6BB5\u8BF4\u660E\uFF1A
2355
- - \`hasReleaseCapability\`: \u662F\u5426\u627E\u5230\u53D1\u5E03\u673A\u5236\uFF08boolean\uFF09
2356
- - \`releaseMethod\`: \u53D1\u5E03\u65B9\u5F0F\uFF0C\u53EF\u9009\u503C\uFF1A"npm-publish" | "ci-pipeline" | "makefile" | "custom-script" | "skill" | "docker" | \u5176\u4ED6\u63CF\u8FF0
2357
- - \`artifacts\`: \u53D1\u73B0\u7684\u53D1\u5E03\u76F8\u5173\u6587\u4EF6\u8DEF\u5F84\u6570\u7EC4
2358
- - \`instructions\`: \u4ECE\u6587\u4EF6\u4E2D\u63D0\u53D6\u7684\u53D1\u5E03\u6B65\u9AA4\u8BF4\u660E\u3002\u5982\u679C\u662F skill/rule \u6587\u4EF6\uFF0C\u8BF7\u5B8C\u6574\u590D\u5236\u5176\u5185\u5BB9
2322
+ - \`hasReleaseCapability\`: **AI \u662F\u5426\u80FD\u76F4\u63A5\u6267\u884C\u53D1\u5E03**\uFF08boolean\uFF09\u2014 \u4EC5\u5F53\u6709\u53EF\u5728\u672C\u5730\u8FD0\u884C\u7684\u53D1\u5E03\u547D\u4EE4\u65F6\u4E3A true
2323
+ - \`releaseMethod\`: \u53D1\u5E03\u65B9\u5F0F\u63CF\u8FF0\uFF1A"npm-publish" | "ci-pipeline" | "makefile" | "custom-script" | "skill" | "docker" | \u5176\u4ED6
2324
+ - \`artifacts\`: \u53D1\u73B0\u7684\u53D1\u5E03\u76F8\u5173\u6587\u4EF6\u8DEF\u5F84\u6570\u7EC4\uFF08\u65E0\u8BBA capability \u662F\u5426\u4E3A true \u90FD\u5217\u51FA\uFF09
2325
+ - \`instructions\`: \u53D1\u5E03\u6B65\u9AA4\u8BF4\u660E\u3002capability \u4E3A false \u65F6\u8BF4\u660E\u539F\u56E0
2359
2326
 
2360
2327
  **\u91CD\u8981**\uFF1A
2361
- - \u5982\u679C\u6CA1\u6709\u627E\u5230\u4EFB\u4F55\u53D1\u5E03\u673A\u5236\uFF0C\u5C06 \`hasReleaseCapability\` \u8BBE\u4E3A \`false\`\uFF0C\u5176\u4ED6\u5B57\u6BB5\u53EF\u7701\u7565
2362
- - \u5982\u679C\u627E\u5230\u4E86 skill \u6216 rule \u6587\u4EF6\uFF0C\u8BF7\u5728 \`instructions\` \u4E2D\u5B8C\u6574\u4FDD\u7559\u539F\u6587\u5185\u5BB9\uFF0C\u540E\u7EED\u53D1\u5E03\u6267\u884C\u5C06\u4F9D\u8D56\u5B83
2328
+ - \u5927\u591A\u6570\u9879\u76EE\u4F9D\u8D56\u5916\u90E8 CI/CD\uFF0C\`hasReleaseCapability\` \u5E94\u4E3A \`false\`
2329
+ - \u5982\u679C\u627E\u5230\u4E86 skill \u6216 rule \u6587\u4EF6\uFF0C\u8BF7\u5728 \`instructions\` \u4E2D\u5B8C\u6574\u4FDD\u7559\u539F\u6587\u5185\u5BB9
2363
2330
  - \u53EA\u8F93\u51FA JSON \u6587\u4EF6\uFF0C\u4E0D\u8981\u6267\u884C\u4EFB\u4F55\u53D1\u5E03\u64CD\u4F5C`;
2364
2331
  }
2365
2332
  function releaseExecPrompt(ctx, detectResult) {
@@ -2382,7 +2349,7 @@ ${instructionSection}
2382
2349
 
2383
2350
  1. \u9605\u8BFB\u4E0A\u8FF0\u53D1\u5E03\u6307\u5F15\u548C\u76F8\u5173\u6587\u4EF6
2384
2351
  2. \u6309\u7167\u9879\u76EE\u5B9A\u4E49\u7684\u53D1\u5E03\u6D41\u7A0B\u6267\u884C\u53D1\u5E03\u64CD\u4F5C
2385
- 3. \u5C06\u53D1\u5E03\u7ED3\u679C\u5199\u5165 \`${pd}/04-release-report.md\`
2352
+ 3. \u5C06\u53D1\u5E03\u7ED3\u679C\u5199\u5165 \`${pd}/06-release-report.md\`
2386
2353
 
2387
2354
  ## \u53D1\u5E03\u62A5\u544A\u683C\u5F0F
2388
2355
 
@@ -2406,38 +2373,27 @@ ${instructionSection}
2406
2373
  }
2407
2374
 
2408
2375
  // src/phases/ReleasePhase.ts
2409
- var logger8 = logger.child("ReleasePhase");
2410
2376
  var DETECT_FILENAME = "05-release-detect.json";
2411
2377
  var REPORT_FILENAME = "06-release-report.md";
2412
2378
  var ReleasePhase = class extends BasePhase {
2413
2379
  phaseName = "release";
2414
- get planDir() {
2415
- const displayId = Number(this.currentCtx?.demand.sourceRef.displayId ?? 0);
2416
- return path7.join(this.plan.baseDir, ".claude-plan", `issue-${displayId}`);
2417
- }
2418
- /** 暂存当前 ctx 供 planDir getter 使用 */
2419
- currentCtx;
2420
2380
  /**
2421
2381
  * 检测结果是否已存在(即是否处于执行模式)。
2422
2382
  */
2423
2383
  hasDetectionResult() {
2424
- return fs6.existsSync(path7.join(this.planDir, DETECT_FILENAME));
2384
+ return this.plan.readFile(DETECT_FILENAME) !== null;
2425
2385
  }
2426
2386
  readDetectionFile() {
2427
- const fp = path7.join(this.planDir, DETECT_FILENAME);
2428
- if (!fs6.existsSync(fp)) return null;
2387
+ const raw = this.plan.readFile(DETECT_FILENAME);
2388
+ if (raw === null) return null;
2429
2389
  try {
2430
- return JSON.parse(fs6.readFileSync(fp, "utf-8"));
2390
+ return JSON.parse(raw);
2431
2391
  } catch {
2432
2392
  return null;
2433
2393
  }
2434
2394
  }
2435
2395
  writeDetectionFile(result) {
2436
- const dir = this.planDir;
2437
- if (!fs6.existsSync(dir)) {
2438
- fs6.mkdirSync(dir, { recursive: true });
2439
- }
2440
- fs6.writeFileSync(path7.join(dir, DETECT_FILENAME), JSON.stringify(result, null, 2), "utf-8");
2396
+ this.plan.writeFile(DETECT_FILENAME, JSON.stringify(result, null, 2));
2441
2397
  }
2442
2398
  getResultFiles(_ctx) {
2443
2399
  if (this.hasDetectionResult()) {
@@ -2460,40 +2416,30 @@ var ReleasePhase = class extends BasePhase {
2460
2416
  }
2461
2417
  return releaseDetectPrompt(pc);
2462
2418
  }
2463
- async execute(ctx) {
2464
- this.currentCtx = ctx;
2465
- const isDetect = !this.hasDetectionResult();
2466
- if (isDetect) {
2419
+ async run(ctx, callbacks) {
2420
+ const needsDetection = !this.hasDetectionResult();
2421
+ if (needsDetection) {
2467
2422
  const cache = new ReleaseDetectCache(resolveDataDir());
2468
2423
  const projectPath = this.config.gongfeng.projectPath;
2469
2424
  const ttlMs = this.config.release.detectCacheTtlMs;
2470
2425
  const cached = cache.get(projectPath, ttlMs);
2471
2426
  if (cached) {
2472
- logger8.info("Using cached release detection result", {
2427
+ this.logger.info("Using cached release detection result", {
2473
2428
  projectPath,
2474
2429
  hasCapability: cached.hasReleaseCapability,
2475
2430
  method: cached.releaseMethod
2476
2431
  });
2477
- await this.hooks.onPhaseStart(this.phaseName);
2478
2432
  this.writeDetectionFile(cached);
2479
- this.plan.updatePhaseProgress(this.phaseName, "completed");
2480
- try {
2481
- await this.git.add(["."]);
2482
- await this.git.commit(`chore: release detection (cached) for issue-${ctx.demand.sourceRef.displayId}`);
2483
- } catch {
2484
- }
2485
- await this.hooks.onPhaseDone(this.phaseName);
2486
2433
  return {
2487
- success: true,
2434
+ status: "completed",
2488
2435
  output: `Release detection cached: ${cached.hasReleaseCapability ? cached.releaseMethod ?? "found" : "no capability"}`,
2489
- exitCode: 0,
2490
- gateRequested: cached.hasReleaseCapability,
2491
- hasReleaseCapability: cached.hasReleaseCapability
2436
+ data: { hasReleaseCapability: cached.hasReleaseCapability }
2492
2437
  };
2493
2438
  }
2494
2439
  }
2495
- const result = await super.execute(ctx);
2496
- if (isDetect && result.success) {
2440
+ const outcome = await super.run(ctx, callbacks);
2441
+ if (outcome.status !== "completed") return outcome;
2442
+ if (needsDetection) {
2497
2443
  const detected = this.readDetectionFile();
2498
2444
  if (detected) {
2499
2445
  const cache = new ReleaseDetectCache(resolveDataDir());
@@ -2503,110 +2449,88 @@ var ReleasePhase = class extends BasePhase {
2503
2449
  projectPath,
2504
2450
  detectedAt: (/* @__PURE__ */ new Date()).toISOString()
2505
2451
  });
2506
- logger8.info("Release detection completed", {
2452
+ this.logger.info("Release detection completed", {
2507
2453
  hasCapability: detected.hasReleaseCapability,
2508
2454
  method: detected.releaseMethod,
2509
2455
  artifacts: detected.artifacts
2510
2456
  });
2511
2457
  return {
2512
- ...result,
2513
- gateRequested: detected.hasReleaseCapability,
2514
- hasReleaseCapability: detected.hasReleaseCapability
2458
+ ...outcome,
2459
+ data: { ...outcome.data, hasReleaseCapability: detected.hasReleaseCapability }
2515
2460
  };
2516
2461
  }
2517
- return { ...result, gateRequested: false, hasReleaseCapability: false };
2462
+ return { ...outcome, data: { ...outcome.data, hasReleaseCapability: false } };
2518
2463
  }
2519
- return { ...result, gateRequested: false };
2464
+ return outcome;
2520
2465
  }
2521
2466
  };
2522
2467
 
2523
2468
  // src/phases/UatPhase.ts
2524
- import fs7 from "fs";
2525
- import path8 from "path";
2526
- var logger9 = logger.child("UatPhase");
2527
2469
  function getDefaultHost() {
2528
2470
  return getLocalIP();
2529
2471
  }
2530
2472
  var UatPhase = class extends BasePhase {
2531
2473
  phaseName = "uat";
2532
- currentCtx;
2533
2474
  getResultFiles(_ctx) {
2534
2475
  return [{ filename: "03-uat-report.md", label: "UAT \u62A5\u544A" }];
2535
2476
  }
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
2477
  /**
2541
2478
  * 检查产物是否已存在且内容足够。
2542
2479
  */
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
- }
2480
+ hasArtifact(_displayId) {
2481
+ return this.plan.isArtifactReady("03-uat-report.md", BasePhase.MIN_ARTIFACT_BYTES);
2551
2482
  }
2552
- async execute(ctx) {
2553
- this.currentCtx = ctx;
2483
+ async run(ctx, callbacks) {
2554
2484
  const displayId = Number(ctx.demand.sourceRef.displayId);
2555
2485
  if (this.hasArtifact(displayId)) {
2556
- return this.completeWithExistingArtifact(ctx, displayId);
2486
+ this.logger.info("UAT artifact found, completing phase", { iid: displayId });
2487
+ try {
2488
+ await this.validatePhaseOutput(ctx, displayId);
2489
+ } catch (err) {
2490
+ return {
2491
+ status: "failed",
2492
+ output: "",
2493
+ error: { message: err.message, isRetryable: false }
2494
+ };
2495
+ }
2496
+ return { status: "completed", output: "UAT report validated" };
2557
2497
  }
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 };
2498
+ return this.launchAsync(ctx, displayId, callbacks);
2572
2499
  }
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"));
2500
+ async launchAsync(ctx, displayId, callbacks) {
2501
+ this.logger.info("Launching async UAT", { iid: displayId });
2582
2502
  const prompt = this.buildPrompt(ctx);
2583
2503
  const runPromise = this.aiRunner.run({
2584
2504
  prompt,
2585
2505
  workDir: this.resolveAIWorkDir(ctx),
2586
2506
  timeoutMs: this.config.ai.phaseTimeoutMs,
2587
2507
  idleTimeoutMs: this.config.ai.idleTimeoutMs,
2588
- onStreamEvent: (event) => {
2589
- eventBus.emitTyped("agent:output", {
2590
- issueIid: displayId,
2591
- phase: this.phaseName,
2592
- event
2593
- });
2594
- }
2508
+ onStreamEvent: callbacks?.onStreamEvent
2595
2509
  });
2596
- runPromise.then(async (result) => {
2510
+ const awaitCompletion = runPromise.then(async (result) => {
2597
2511
  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);
2512
+ return {
2513
+ status: "completed",
2514
+ output: result.output,
2515
+ sessionId: result.sessionId
2516
+ };
2604
2517
  }
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 };
2518
+ const reason = result.errorMessage || "UAT failed or artifact 03-uat-report.md missing";
2519
+ return {
2520
+ status: "failed",
2521
+ output: result.output,
2522
+ error: { message: reason, isRetryable: false }
2523
+ };
2524
+ }).catch(async (err) => ({
2525
+ status: "failed",
2526
+ output: "",
2527
+ error: { message: err.message, isRetryable: false }
2528
+ }));
2529
+ return {
2530
+ status: "running",
2531
+ output: "UAT launched asynchronously",
2532
+ awaitCompletion
2533
+ };
2610
2534
  }
2611
2535
  buildPrompt(ctx) {
2612
2536
  const pc = demandToPromptContext(ctx.demand);
@@ -2617,8 +2541,7 @@ var UatPhase = class extends BasePhase {
2617
2541
  workspace: ctx.workspace
2618
2542
  };
2619
2543
  const hasUatTool = !!this.config.e2e.uatVendorDir;
2620
- const systemE2e = this.hooks.isE2eEnabled();
2621
- const e2ePorts = systemE2e && ctx.ports ? {
2544
+ const e2ePorts = ctx.ports ? {
2622
2545
  backendPort: ctx.ports.backendPort,
2623
2546
  frontendPort: ctx.ports.frontendPort,
2624
2547
  host: this.config.preview.host || getDefaultHost()
@@ -2786,9 +2709,9 @@ function createLifecycleManager(def) {
2786
2709
 
2787
2710
  // src/workspace/WorkspaceConfig.ts
2788
2711
  import { z } from "zod";
2789
- import fs8 from "fs";
2790
- import path9 from "path";
2791
- var logger10 = logger.child("WorkspaceConfig");
2712
+ import fs4 from "fs";
2713
+ import path6 from "path";
2714
+ var logger8 = logger.child("WorkspaceConfig");
2792
2715
  var repoConfigSchema = z.object({
2793
2716
  name: z.string().min(1, "Repo name is required"),
2794
2717
  projectPath: z.string().min(1, "Gongfeng project path is required"),
@@ -2804,29 +2727,29 @@ var workspaceConfigSchema = z.object({
2804
2727
  });
2805
2728
  function loadWorkspaceConfig(configPath) {
2806
2729
  if (!configPath) return null;
2807
- if (!fs8.existsSync(configPath)) {
2808
- logger10.warn("Workspace config file not found, falling back to single-repo mode", {
2730
+ if (!fs4.existsSync(configPath)) {
2731
+ logger8.warn("Workspace config file not found, falling back to single-repo mode", {
2809
2732
  path: configPath
2810
2733
  });
2811
2734
  return null;
2812
2735
  }
2813
2736
  try {
2814
- const raw = fs8.readFileSync(configPath, "utf-8");
2737
+ const raw = fs4.readFileSync(configPath, "utf-8");
2815
2738
  const json = JSON.parse(raw);
2816
2739
  const result = workspaceConfigSchema.safeParse(json);
2817
2740
  if (!result.success) {
2818
2741
  const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
2819
- logger10.error(`Workspace config validation failed:
2742
+ logger8.error(`Workspace config validation failed:
2820
2743
  ${issues}`);
2821
2744
  return null;
2822
2745
  }
2823
- logger10.info("Workspace config loaded", {
2746
+ logger8.info("Workspace config loaded", {
2824
2747
  primary: result.data.primary.name,
2825
2748
  associates: result.data.associates.map((a) => a.name)
2826
2749
  });
2827
2750
  return result.data;
2828
2751
  } catch (err) {
2829
- logger10.error("Failed to parse workspace config", {
2752
+ logger8.error("Failed to parse workspace config", {
2830
2753
  path: configPath,
2831
2754
  error: err.message
2832
2755
  });
@@ -2852,14 +2775,14 @@ function isMultiRepo(ws) {
2852
2775
  }
2853
2776
  function persistWorkspaceConfig(ws, filePath) {
2854
2777
  try {
2855
- const dir = path9.dirname(filePath);
2856
- if (!fs8.existsSync(dir)) {
2857
- fs8.mkdirSync(dir, { recursive: true });
2778
+ const dir = path6.dirname(filePath);
2779
+ if (!fs4.existsSync(dir)) {
2780
+ fs4.mkdirSync(dir, { recursive: true });
2858
2781
  }
2859
- fs8.writeFileSync(filePath, JSON.stringify(ws, null, 2) + "\n", "utf-8");
2860
- logger10.info("Workspace config auto-generated from .env", { path: filePath });
2782
+ fs4.writeFileSync(filePath, JSON.stringify(ws, null, 2) + "\n", "utf-8");
2783
+ logger8.info("Workspace config auto-generated from .env", { path: filePath });
2861
2784
  } catch (err) {
2862
- logger10.warn("Failed to persist workspace config", {
2785
+ logger8.warn("Failed to persist workspace config", {
2863
2786
  path: filePath,
2864
2787
  error: err.message
2865
2788
  });
@@ -2867,12 +2790,12 @@ function persistWorkspaceConfig(ws, filePath) {
2867
2790
  }
2868
2791
 
2869
2792
  // src/workspace/WorkspaceManager.ts
2870
- import path10 from "path";
2871
- import fs9 from "fs/promises";
2793
+ import path7 from "path";
2794
+ import fs5 from "fs/promises";
2872
2795
  import { execFile } from "child_process";
2873
2796
  import { promisify } from "util";
2874
2797
  var execFileAsync = promisify(execFile);
2875
- var logger11 = logger.child("WorkspaceManager");
2798
+ var logger9 = logger.child("WorkspaceManager");
2876
2799
  var WorkspaceManager = class {
2877
2800
  wsConfig;
2878
2801
  worktreeBaseDir;
@@ -2901,7 +2824,7 @@ var WorkspaceManager = class {
2901
2824
  */
2902
2825
  async prepareWorkspace(issueIid, branchName, globalBaseBranch, globalBranchPrefix) {
2903
2826
  const wsRoot = this.getWorkspaceRoot(issueIid);
2904
- await fs9.mkdir(wsRoot, { recursive: true });
2827
+ await fs5.mkdir(wsRoot, { recursive: true });
2905
2828
  const primaryCtx = await this.preparePrimaryRepo(
2906
2829
  issueIid,
2907
2830
  branchName,
@@ -2920,7 +2843,7 @@ var WorkspaceManager = class {
2920
2843
  );
2921
2844
  associateCtxs.push(ctx);
2922
2845
  }
2923
- logger11.info("Workspace prepared", {
2846
+ logger9.info("Workspace prepared", {
2924
2847
  issueIid,
2925
2848
  wsRoot,
2926
2849
  repos: [primaryCtx.name, ...associateCtxs.map((a) => a.name)]
@@ -2945,7 +2868,7 @@ var WorkspaceManager = class {
2945
2868
  await git.commit(message);
2946
2869
  await git.push(wsCtx.branchName);
2947
2870
  committed.push(repo.name);
2948
- logger11.info("Committed and pushed changes", {
2871
+ logger9.info("Committed and pushed changes", {
2949
2872
  repo: repo.name,
2950
2873
  branch: wsCtx.branchName
2951
2874
  });
@@ -2959,19 +2882,19 @@ var WorkspaceManager = class {
2959
2882
  async cleanupWorkspace(wsCtx) {
2960
2883
  try {
2961
2884
  await this.mainGit.worktreeRemove(wsCtx.primary.gitRootDir, true);
2962
- logger11.info("Primary worktree removed", { dir: wsCtx.primary.gitRootDir });
2885
+ logger9.info("Primary worktree removed", { dir: wsCtx.primary.gitRootDir });
2963
2886
  } catch (err) {
2964
- logger11.warn("Failed to remove primary worktree", {
2887
+ logger9.warn("Failed to remove primary worktree", {
2965
2888
  dir: wsCtx.primary.gitRootDir,
2966
2889
  error: err.message
2967
2890
  });
2968
2891
  }
2969
2892
  for (const assoc of wsCtx.associates) {
2970
2893
  try {
2971
- await fs9.rm(assoc.gitRootDir, { recursive: true, force: true });
2972
- logger11.info("Associate repo dir removed", { name: assoc.name, dir: assoc.gitRootDir });
2894
+ await fs5.rm(assoc.gitRootDir, { recursive: true, force: true });
2895
+ logger9.info("Associate repo dir removed", { name: assoc.name, dir: assoc.gitRootDir });
2973
2896
  } catch (err) {
2974
- logger11.warn("Failed to remove associate repo dir", {
2897
+ logger9.warn("Failed to remove associate repo dir", {
2975
2898
  name: assoc.name,
2976
2899
  dir: assoc.gitRootDir,
2977
2900
  error: err.message
@@ -2979,9 +2902,9 @@ var WorkspaceManager = class {
2979
2902
  }
2980
2903
  }
2981
2904
  try {
2982
- const entries = await fs9.readdir(wsCtx.workspaceRoot);
2905
+ const entries = await fs5.readdir(wsCtx.workspaceRoot);
2983
2906
  if (entries.length === 0) {
2984
- await fs9.rmdir(wsCtx.workspaceRoot);
2907
+ await fs5.rmdir(wsCtx.workspaceRoot);
2985
2908
  }
2986
2909
  } catch {
2987
2910
  }
@@ -2993,13 +2916,13 @@ var WorkspaceManager = class {
2993
2916
  const wsRoot = this.getWorkspaceRoot(issueIid);
2994
2917
  const primary = this.wsConfig.primary;
2995
2918
  const defaultPrefix = globalBranchPrefix ?? primary.branchPrefix ?? "feat/issue";
2996
- const primaryDir = path10.join(wsRoot, primary.name);
2919
+ const primaryDir = path7.join(wsRoot, primary.name);
2997
2920
  const repos = [{
2998
2921
  name: primary.name,
2999
2922
  projectPath: primary.projectPath,
3000
2923
  role: primary.role ?? "",
3001
2924
  gitRootDir: primaryDir,
3002
- workDir: path10.join(primaryDir, primary.projectSubDir ?? ""),
2925
+ workDir: path7.join(primaryDir, primary.projectSubDir ?? ""),
3003
2926
  baseBranch: primary.baseBranch ?? globalBaseBranch,
3004
2927
  branchPrefix: primary.branchPrefix ?? defaultPrefix,
3005
2928
  isPrimary: true
@@ -3009,8 +2932,8 @@ var WorkspaceManager = class {
3009
2932
  name: assoc.name,
3010
2933
  projectPath: assoc.projectPath,
3011
2934
  role: assoc.role ?? "",
3012
- gitRootDir: path10.join(wsRoot, assoc.name),
3013
- workDir: path10.join(wsRoot, assoc.name, assoc.projectSubDir ?? ""),
2935
+ gitRootDir: path7.join(wsRoot, assoc.name),
2936
+ workDir: path7.join(wsRoot, assoc.name, assoc.projectSubDir ?? ""),
3014
2937
  baseBranch: assoc.baseBranch ?? globalBaseBranch,
3015
2938
  branchPrefix: assoc.branchPrefix ?? defaultPrefix,
3016
2939
  isPrimary: false
@@ -3019,12 +2942,12 @@ var WorkspaceManager = class {
3019
2942
  return repos;
3020
2943
  }
3021
2944
  getWorkspaceRoot(issueIid) {
3022
- return path10.join(this.worktreeBaseDir, `issue-${issueIid}`);
2945
+ return path7.join(this.worktreeBaseDir, `issue-${issueIid}`);
3023
2946
  }
3024
2947
  // ── Internal helpers ──
3025
2948
  async preparePrimaryRepo(issueIid, branchName, wsRoot, globalBaseBranch) {
3026
2949
  const primary = this.wsConfig.primary;
3027
- const repoDir = path10.join(wsRoot, primary.name);
2950
+ const repoDir = path7.join(wsRoot, primary.name);
3028
2951
  const baseBranch = primary.baseBranch ?? globalBaseBranch;
3029
2952
  await this.ensurePrimaryWorktree(repoDir, branchName, baseBranch);
3030
2953
  return {
@@ -3032,18 +2955,18 @@ var WorkspaceManager = class {
3032
2955
  projectPath: primary.projectPath,
3033
2956
  role: primary.role ?? "",
3034
2957
  gitRootDir: repoDir,
3035
- workDir: path10.join(repoDir, primary.projectSubDir ?? ""),
2958
+ workDir: path7.join(repoDir, primary.projectSubDir ?? ""),
3036
2959
  baseBranch,
3037
2960
  branchPrefix: primary.branchPrefix ?? "feat/issue",
3038
2961
  isPrimary: true
3039
2962
  };
3040
2963
  }
3041
2964
  async ensurePrimaryWorktree(repoDir, branchName, baseBranch) {
3042
- const wsRoot = path10.dirname(repoDir);
2965
+ const wsRoot = path7.dirname(repoDir);
3043
2966
  if (wsRoot !== repoDir) {
3044
2967
  try {
3045
- await fs9.access(path10.join(wsRoot, ".git"));
3046
- logger11.info("Migrating legacy worktree to primary subdir", { from: wsRoot, to: repoDir });
2968
+ await fs5.access(path7.join(wsRoot, ".git"));
2969
+ logger9.info("Migrating legacy worktree to primary subdir", { from: wsRoot, to: repoDir });
3047
2970
  await this.mainGit.worktreeRemove(wsRoot, true);
3048
2971
  await this.mainGit.worktreePrune();
3049
2972
  await this.cleanStaleDir(wsRoot);
@@ -3053,11 +2976,11 @@ var WorkspaceManager = class {
3053
2976
  const worktrees = await this.mainGit.worktreeList();
3054
2977
  if (worktrees.includes(repoDir)) {
3055
2978
  try {
3056
- await fs9.access(path10.join(repoDir, ".git"));
3057
- logger11.info("Reusing existing primary worktree", { dir: repoDir });
2979
+ await fs5.access(path7.join(repoDir, ".git"));
2980
+ logger9.info("Reusing existing primary worktree", { dir: repoDir });
3058
2981
  return;
3059
2982
  } catch {
3060
- logger11.warn("Primary worktree registered but .git missing, recreating", { dir: repoDir });
2983
+ logger9.warn("Primary worktree registered but .git missing, recreating", { dir: repoDir });
3061
2984
  await this.mainGit.worktreeRemove(repoDir, true);
3062
2985
  await this.mainGit.worktreePrune();
3063
2986
  }
@@ -3076,19 +2999,19 @@ var WorkspaceManager = class {
3076
2999
  await this.mainGit.worktreeAdd(repoDir, branchName, `origin/${baseBranch}`);
3077
3000
  }
3078
3001
  async prepareAssociateRepo(assoc, _issueIid, branchName, wsRoot, globalBaseBranch, globalBranchPrefix) {
3079
- const repoDir = path10.join(wsRoot, assoc.name);
3002
+ const repoDir = path7.join(wsRoot, assoc.name);
3080
3003
  const baseBranch = assoc.baseBranch ?? globalBaseBranch;
3081
3004
  const cloneUrl = `${this.gongfengApiUrl}/${assoc.projectPath}.git`;
3082
- const gitDirExists = await this.dirExists(path10.join(repoDir, ".git"));
3005
+ const gitDirExists = await this.dirExists(path7.join(repoDir, ".git"));
3083
3006
  if (!gitDirExists) {
3084
3007
  await this.cleanStaleDir(repoDir);
3085
- logger11.info("Cloning associate repo", { name: assoc.name, url: cloneUrl });
3008
+ logger9.info("Cloning associate repo", { name: assoc.name, url: cloneUrl });
3086
3009
  await execFileAsync("git", ["clone", "--depth", "50", cloneUrl, repoDir], {
3087
3010
  timeout: 3e5,
3088
3011
  maxBuffer: 10 * 1024 * 1024
3089
3012
  });
3090
3013
  } else {
3091
- logger11.info("Reusing existing associate clone", { name: assoc.name, dir: repoDir });
3014
+ logger9.info("Reusing existing associate clone", { name: assoc.name, dir: repoDir });
3092
3015
  }
3093
3016
  const assocGit = new GitOperations(repoDir);
3094
3017
  await assocGit.fetch();
@@ -3111,7 +3034,7 @@ var WorkspaceManager = class {
3111
3034
  projectPath: assoc.projectPath,
3112
3035
  role: assoc.role ?? "",
3113
3036
  gitRootDir: repoDir,
3114
- workDir: path10.join(repoDir, assoc.projectSubDir ?? ""),
3037
+ workDir: path7.join(repoDir, assoc.projectSubDir ?? ""),
3115
3038
  baseBranch,
3116
3039
  branchPrefix: assoc.branchPrefix ?? globalBranchPrefix,
3117
3040
  isPrimary: false
@@ -3119,13 +3042,13 @@ var WorkspaceManager = class {
3119
3042
  }
3120
3043
  async cleanStaleDir(dir) {
3121
3044
  if (await this.dirExists(dir)) {
3122
- logger11.warn("Removing stale directory", { dir });
3123
- await fs9.rm(dir, { recursive: true, force: true });
3045
+ logger9.warn("Removing stale directory", { dir });
3046
+ await fs5.rm(dir, { recursive: true, force: true });
3124
3047
  }
3125
3048
  }
3126
3049
  async dirExists(dir) {
3127
3050
  try {
3128
- await fs9.access(dir);
3051
+ await fs5.access(dir);
3129
3052
  return true;
3130
3053
  } catch {
3131
3054
  return false;
@@ -3134,8 +3057,8 @@ var WorkspaceManager = class {
3134
3057
  };
3135
3058
 
3136
3059
  // src/orchestrator/PipelineOrchestrator.ts
3137
- import path14 from "path";
3138
- import fs13 from "fs/promises";
3060
+ import path11 from "path";
3061
+ import fs9 from "fs/promises";
3139
3062
  import fsSync from "fs";
3140
3063
  import { execFile as execFile2 } from "child_process";
3141
3064
  import { promisify as promisify2 } from "util";
@@ -3168,8 +3091,8 @@ function mapSupplement(s) {
3168
3091
  }
3169
3092
 
3170
3093
  // src/utils/MergeRequestHelper.ts
3171
- import fs10 from "fs";
3172
- import path11 from "path";
3094
+ import fs6 from "fs";
3095
+ import path8 from "path";
3173
3096
  var TAPD_PATTERNS = [
3174
3097
  /--story=(\d+)/i,
3175
3098
  /--bug=(\d+)/i,
@@ -3213,9 +3136,9 @@ function generateMRDescription(options) {
3213
3136
  ];
3214
3137
  const planSections = [];
3215
3138
  for (const { filename, label } of summaryFiles) {
3216
- const filePath = path11.join(planDir, ".claude-plan", `issue-${issueIid}`, filename);
3217
- if (fs10.existsSync(filePath)) {
3218
- const content = fs10.readFileSync(filePath, "utf-8");
3139
+ const filePath = path8.join(planDir, ".claude-plan", `issue-${issueIid}`, filename);
3140
+ if (fs6.existsSync(filePath)) {
3141
+ const content = fs6.readFileSync(filePath, "utf-8");
3219
3142
  const summary = extractSummary(content);
3220
3143
  if (summary) {
3221
3144
  planSections.push(`### ${label}
@@ -3239,7 +3162,7 @@ function extractSummary(content, maxLines = 20) {
3239
3162
 
3240
3163
  // src/deploy/PortAllocator.ts
3241
3164
  import net from "net";
3242
- var logger12 = logger.child("PortAllocator");
3165
+ var logger10 = logger.child("PortAllocator");
3243
3166
  var DEFAULT_OPTIONS = {
3244
3167
  backendPortBase: 4e3,
3245
3168
  frontendPortBase: 9e3,
@@ -3264,7 +3187,7 @@ var PortAllocator = class {
3264
3187
  async allocate(issueIid) {
3265
3188
  const existing = this.allocated.get(issueIid);
3266
3189
  if (existing) {
3267
- logger12.info("Returning already allocated ports", { issueIid, ports: existing });
3190
+ logger10.info("Returning already allocated ports", { issueIid, ports: existing });
3268
3191
  return existing;
3269
3192
  }
3270
3193
  const usedBackend = new Set([...this.allocated.values()].map((p) => p.backendPort));
@@ -3282,10 +3205,10 @@ var PortAllocator = class {
3282
3205
  if (beOk && feOk) {
3283
3206
  const pair = { backendPort, frontendPort };
3284
3207
  this.allocated.set(issueIid, pair);
3285
- logger12.info("Ports allocated", { issueIid, ...pair });
3208
+ logger10.info("Ports allocated", { issueIid, ...pair });
3286
3209
  return pair;
3287
3210
  }
3288
- logger12.debug("Port pair unavailable, trying next", {
3211
+ logger10.debug("Port pair unavailable, trying next", {
3289
3212
  backendPort,
3290
3213
  frontendPort,
3291
3214
  beOk,
@@ -3300,7 +3223,7 @@ var PortAllocator = class {
3300
3223
  const pair = this.allocated.get(issueIid);
3301
3224
  if (pair) {
3302
3225
  this.allocated.delete(issueIid);
3303
- logger12.info("Ports released", { issueIid, ...pair });
3226
+ logger10.info("Ports released", { issueIid, ...pair });
3304
3227
  }
3305
3228
  }
3306
3229
  getPortsForIssue(issueIid) {
@@ -3311,15 +3234,15 @@ var PortAllocator = class {
3311
3234
  }
3312
3235
  restore(issueIid, ports) {
3313
3236
  this.allocated.set(issueIid, ports);
3314
- logger12.info("Ports restored from persistence", { issueIid, ...ports });
3237
+ logger10.info("Ports restored from persistence", { issueIid, ...ports });
3315
3238
  }
3316
3239
  };
3317
3240
 
3318
3241
  // src/deploy/DevServerManager.ts
3319
3242
  import { spawn } from "child_process";
3320
- import fs11 from "fs";
3321
- import path12 from "path";
3322
- var logger13 = logger.child("DevServerManager");
3243
+ import fs7 from "fs";
3244
+ import path9 from "path";
3245
+ var logger11 = logger.child("DevServerManager");
3323
3246
  var DEFAULT_OPTIONS2 = {};
3324
3247
  var DevServerManager = class {
3325
3248
  servers = /* @__PURE__ */ new Map();
@@ -3327,25 +3250,25 @@ var DevServerManager = class {
3327
3250
  logDir;
3328
3251
  constructor(options) {
3329
3252
  this.options = { ...DEFAULT_OPTIONS2, ...options };
3330
- this.logDir = path12.join(resolveDataDir(), "preview-logs");
3331
- if (!fs11.existsSync(this.logDir)) {
3332
- fs11.mkdirSync(this.logDir, { recursive: true });
3253
+ this.logDir = path9.join(resolveDataDir(), "preview-logs");
3254
+ if (!fs7.existsSync(this.logDir)) {
3255
+ fs7.mkdirSync(this.logDir, { recursive: true });
3333
3256
  }
3334
3257
  }
3335
3258
  getLogPath(issueIid, type) {
3336
- const filePath = path12.join(this.logDir, `${issueIid}-${type}.log`);
3337
- return fs11.existsSync(filePath) ? filePath : null;
3259
+ const filePath = path9.join(this.logDir, `${issueIid}-${type}.log`);
3260
+ return fs7.existsSync(filePath) ? filePath : null;
3338
3261
  }
3339
3262
  async startServers(wtCtx, ports) {
3340
3263
  if (this.servers.has(wtCtx.issueIid)) {
3341
- logger13.info("Servers already running for issue", { issueIid: wtCtx.issueIid });
3264
+ logger11.info("Servers already running for issue", { issueIid: wtCtx.issueIid });
3342
3265
  return;
3343
3266
  }
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" });
3267
+ logger11.info("Starting dev servers", { issueIid: wtCtx.issueIid, ...ports });
3268
+ const backendLogPath = path9.join(this.logDir, `${wtCtx.issueIid}-backend.log`);
3269
+ const frontendLogPath = path9.join(this.logDir, `${wtCtx.issueIid}-frontend.log`);
3270
+ const backendLog = fs7.createWriteStream(backendLogPath, { flags: "a" });
3271
+ const frontendLog = fs7.createWriteStream(frontendLogPath, { flags: "a" });
3349
3272
  const tsLine = (stream, data) => `[${(/* @__PURE__ */ new Date()).toISOString()}] [${stream}] ${data.toString().trimEnd()}
3350
3273
  `;
3351
3274
  const backendEnv = {
@@ -3369,9 +3292,9 @@ var DevServerManager = class {
3369
3292
  backendLog.write(tsLine("stderr", data));
3370
3293
  });
3371
3294
  backend.on("exit", (code) => {
3372
- logger13.info("Backend process exited", { issueIid: wtCtx.issueIid, code });
3295
+ logger11.info("Backend process exited", { issueIid: wtCtx.issueIid, code });
3373
3296
  });
3374
- const frontendDir = path12.join(wtCtx.workDir, "frontend");
3297
+ const frontendDir = path9.join(wtCtx.workDir, "frontend");
3375
3298
  const frontendEnv = {
3376
3299
  ...process.env,
3377
3300
  BACKEND_PORT: String(ports.backendPort),
@@ -3393,7 +3316,7 @@ var DevServerManager = class {
3393
3316
  frontendLog.write(tsLine("stderr", data));
3394
3317
  });
3395
3318
  frontend.on("exit", (code) => {
3396
- logger13.info("Frontend process exited", { issueIid: wtCtx.issueIid, code });
3319
+ logger11.info("Frontend process exited", { issueIid: wtCtx.issueIid, code });
3397
3320
  });
3398
3321
  const serverSet = {
3399
3322
  backend,
@@ -3405,14 +3328,14 @@ var DevServerManager = class {
3405
3328
  frontendLog
3406
3329
  };
3407
3330
  this.servers.set(wtCtx.issueIid, serverSet);
3408
- logger13.info("Dev servers spawned, waiting for startup", { issueIid: wtCtx.issueIid, ...ports });
3331
+ logger11.info("Dev servers spawned, waiting for startup", { issueIid: wtCtx.issueIid, ...ports });
3409
3332
  await new Promise((r) => setTimeout(r, 1e4));
3410
- logger13.info("Dev servers startup grace period done", { issueIid: wtCtx.issueIid });
3333
+ logger11.info("Dev servers startup grace period done", { issueIid: wtCtx.issueIid });
3411
3334
  }
3412
3335
  stopServers(issueIid) {
3413
3336
  const set = this.servers.get(issueIid);
3414
3337
  if (!set) return;
3415
- logger13.info("Stopping dev servers", { issueIid, ports: set.ports });
3338
+ logger11.info("Stopping dev servers", { issueIid, ports: set.ports });
3416
3339
  killProcess(set.backend, `backend #${issueIid}`);
3417
3340
  killProcess(set.frontend, `frontend #${issueIid}`);
3418
3341
  set.backendLog.end();
@@ -3449,7 +3372,7 @@ function killProcess(proc, label) {
3449
3372
  }
3450
3373
  setTimeout(() => {
3451
3374
  if (!proc.killed && proc.exitCode === null) {
3452
- logger13.warn(`Force killing ${label}`);
3375
+ logger11.warn(`Force killing ${label}`);
3453
3376
  try {
3454
3377
  process.kill(-pid, "SIGKILL");
3455
3378
  } catch {
@@ -3458,7 +3381,7 @@ function killProcess(proc, label) {
3458
3381
  }
3459
3382
  }, 5e3);
3460
3383
  } catch (err) {
3461
- logger13.warn(`Failed to kill ${label}`, { error: err.message });
3384
+ logger11.warn(`Failed to kill ${label}`, { error: err.message });
3462
3385
  }
3463
3386
  }
3464
3387
 
@@ -3477,13 +3400,13 @@ function isE2eEnabledForIssue(issueIid, tracker, cfg) {
3477
3400
  }
3478
3401
 
3479
3402
  // src/e2e/ScreenshotCollector.ts
3480
- import fs12 from "fs";
3481
- import path13 from "path";
3482
- var logger14 = logger.child("ScreenshotCollector");
3403
+ import fs8 from "fs";
3404
+ import path10 from "path";
3405
+ var logger12 = logger.child("ScreenshotCollector");
3483
3406
  var MAX_SCREENSHOTS = 20;
3484
3407
  function walkDir(dir, files = []) {
3485
- for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
3486
- const full = path13.join(dir, entry.name);
3408
+ for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
3409
+ const full = path10.join(dir, entry.name);
3487
3410
  if (entry.isDirectory()) {
3488
3411
  walkDir(full, files);
3489
3412
  } else if (entry.isFile() && entry.name.endsWith(".png")) {
@@ -3493,34 +3416,34 @@ function walkDir(dir, files = []) {
3493
3416
  return files;
3494
3417
  }
3495
3418
  function collectScreenshots(workDir) {
3496
- const testResultsDir = path13.join(workDir, "frontend", "test-results");
3497
- if (!fs12.existsSync(testResultsDir)) {
3498
- logger14.debug("test-results directory not found", { dir: testResultsDir });
3419
+ const testResultsDir = path10.join(workDir, "frontend", "test-results");
3420
+ if (!fs8.existsSync(testResultsDir)) {
3421
+ logger12.debug("test-results directory not found", { dir: testResultsDir });
3499
3422
  return [];
3500
3423
  }
3501
3424
  const pngFiles = walkDir(testResultsDir);
3502
3425
  if (pngFiles.length === 0) {
3503
- logger14.debug("No screenshots found");
3426
+ logger12.debug("No screenshots found");
3504
3427
  return [];
3505
3428
  }
3506
3429
  const screenshots = pngFiles.map((filePath) => {
3507
- const relative = path13.relative(testResultsDir, filePath);
3508
- const testName = relative.split(path13.sep)[0] || path13.basename(filePath, ".png");
3430
+ const relative = path10.relative(testResultsDir, filePath);
3431
+ const testName = relative.split(path10.sep)[0] || path10.basename(filePath, ".png");
3509
3432
  return { filePath, testName };
3510
3433
  });
3511
3434
  if (screenshots.length > MAX_SCREENSHOTS) {
3512
- logger14.warn("Too many screenshots, truncating", {
3435
+ logger12.warn("Too many screenshots, truncating", {
3513
3436
  total: screenshots.length,
3514
3437
  max: MAX_SCREENSHOTS
3515
3438
  });
3516
3439
  return screenshots.slice(0, MAX_SCREENSHOTS);
3517
3440
  }
3518
- logger14.info("Screenshots collected", { count: screenshots.length });
3441
+ logger12.info("Screenshots collected", { count: screenshots.length });
3519
3442
  return screenshots;
3520
3443
  }
3521
3444
 
3522
3445
  // src/e2e/ScreenshotPublisher.ts
3523
- var logger15 = logger.child("ScreenshotPublisher");
3446
+ var logger13 = logger.child("ScreenshotPublisher");
3524
3447
  function buildComment(uploaded, truncated) {
3525
3448
  const lines = [t("screenshot.title"), ""];
3526
3449
  for (const item of uploaded) {
@@ -3539,12 +3462,12 @@ var ScreenshotPublisher = class {
3539
3462
  const { workDir, issueIid, issueId, mrIid } = options;
3540
3463
  const screenshots = collectScreenshots(workDir);
3541
3464
  if (screenshots.length === 0) {
3542
- logger15.info("No E2E screenshots to publish", { issueIid });
3465
+ logger13.info("No E2E screenshots to publish", { issueIid });
3543
3466
  return;
3544
3467
  }
3545
3468
  const uploaded = await this.uploadAll(screenshots);
3546
3469
  if (uploaded.length === 0) {
3547
- logger15.warn("All screenshot uploads failed", { issueIid });
3470
+ logger13.warn("All screenshot uploads failed", { issueIid });
3548
3471
  return;
3549
3472
  }
3550
3473
  const truncated = screenshots.length >= 20;
@@ -3553,7 +3476,7 @@ var ScreenshotPublisher = class {
3553
3476
  if (mrIid) {
3554
3477
  await this.postToMergeRequest(mrIid, comment);
3555
3478
  }
3556
- logger15.info("E2E screenshots published", {
3479
+ logger13.info("E2E screenshots published", {
3557
3480
  issueIid,
3558
3481
  mrIid,
3559
3482
  count: uploaded.length
@@ -3569,7 +3492,7 @@ var ScreenshotPublisher = class {
3569
3492
  markdown: result.markdown
3570
3493
  });
3571
3494
  } catch (err) {
3572
- logger15.warn("Failed to upload screenshot", {
3495
+ logger13.warn("Failed to upload screenshot", {
3573
3496
  filePath: screenshot.filePath,
3574
3497
  error: err.message
3575
3498
  });
@@ -3581,7 +3504,7 @@ var ScreenshotPublisher = class {
3581
3504
  try {
3582
3505
  await this.gongfeng.createIssueNote(issueId, comment);
3583
3506
  } catch (err) {
3584
- logger15.warn("Failed to post screenshots to issue", {
3507
+ logger13.warn("Failed to post screenshots to issue", {
3585
3508
  issueId,
3586
3509
  error: err.message
3587
3510
  });
@@ -3591,7 +3514,7 @@ var ScreenshotPublisher = class {
3591
3514
  try {
3592
3515
  await this.gongfeng.createMergeRequestNote(mrIid, comment);
3593
3516
  } catch (err) {
3594
- logger15.warn("Failed to post screenshots to merge request", {
3517
+ logger13.warn("Failed to post screenshots to merge request", {
3595
3518
  mrIid,
3596
3519
  error: err.message
3597
3520
  });
@@ -3774,7 +3697,7 @@ metrics.registerCounter("iaf_braindump_batches_total", "Total braindump batches"
3774
3697
  metrics.registerCounter("iaf_braindump_tasks_total", "Total braindump tasks");
3775
3698
 
3776
3699
  // src/orchestrator/steps/SetupStep.ts
3777
- var logger16 = logger.child("SetupStep");
3700
+ var logger14 = logger.child("SetupStep");
3778
3701
  async function executeSetup(ctx, deps) {
3779
3702
  const { issue, wtCtx, record, pipelineDef, branchName } = ctx;
3780
3703
  try {
@@ -3783,7 +3706,7 @@ async function executeSetup(ctx, deps) {
3783
3706
  "auto-finish:processing"
3784
3707
  ]);
3785
3708
  } catch (err) {
3786
- logger16.warn("Failed to update issue labels", { error: err.message });
3709
+ logger14.warn("Failed to update issue labels", { error: err.message });
3787
3710
  }
3788
3711
  await deps.mainGitMutex.runExclusive(async () => {
3789
3712
  deps.emitProgress(issue.iid, "fetch", t("orchestrator.fetchProgress"));
@@ -3817,11 +3740,14 @@ async function executeSetup(ctx, deps) {
3817
3740
  state: issue.state
3818
3741
  });
3819
3742
  const existingProgress = wtPlan.readProgress();
3820
- if (!existingProgress) {
3743
+ if (!existingProgress || !ctx.isRetry) {
3821
3744
  wtPlan.writeProgress(
3822
3745
  wtPlan.createInitialProgress(issue.iid, issue.title, branchName, pipelineDef)
3823
3746
  );
3824
3747
  }
3748
+ if (!record.phaseProgress) {
3749
+ deps.tracker.initPhaseProgress(issue.iid, pipelineDef);
3750
+ }
3825
3751
  const wtGitMap = /* @__PURE__ */ new Map();
3826
3752
  if (wtCtx.workspace) {
3827
3753
  wtGitMap.set(wtCtx.workspace.primary.name, wtGit);
@@ -3834,8 +3760,52 @@ async function executeSetup(ctx, deps) {
3834
3760
  return { wtGit, wtPlan, wtGitMap };
3835
3761
  }
3836
3762
 
3763
+ // src/notesync/NoteSyncSettings.ts
3764
+ var noteSyncOverride;
3765
+ function getNoteSyncEnabled(cfg) {
3766
+ return noteSyncOverride ?? cfg.issueNoteSync.enabled;
3767
+ }
3768
+ function setNoteSyncOverride(value) {
3769
+ noteSyncOverride = value;
3770
+ }
3771
+ function isNoteSyncEnabledForIssue(issueIid, tracker, cfg) {
3772
+ const record = tracker.get(issueIid);
3773
+ if (record?.issueNoteSyncEnabled !== void 0) return record.issueNoteSyncEnabled;
3774
+ return getNoteSyncEnabled(cfg);
3775
+ }
3776
+ var SUMMARY_MAX_LENGTH = 500;
3777
+ function truncateToSummary(content) {
3778
+ if (content.length <= SUMMARY_MAX_LENGTH) return content;
3779
+ const cut = content.slice(0, SUMMARY_MAX_LENGTH);
3780
+ const lastNewline = cut.lastIndexOf("\n\n");
3781
+ const boundary = lastNewline > SUMMARY_MAX_LENGTH * 0.3 ? lastNewline : cut.lastIndexOf("\n");
3782
+ const summary = boundary > SUMMARY_MAX_LENGTH * 0.3 ? cut.slice(0, boundary) : cut;
3783
+ return summary + "\n\n...";
3784
+ }
3785
+ function buildNoteSyncComment(phaseName, phaseLabel, docUrl, dashboardUrl, summary) {
3786
+ const emoji = {
3787
+ analysis: "\u{1F50D}",
3788
+ design: "\u{1F4D0}",
3789
+ implement: "\u{1F4BB}",
3790
+ verify: "\u2705",
3791
+ plan: "\u{1F4CB}",
3792
+ review: "\u{1F440}",
3793
+ build: "\u{1F528}"
3794
+ };
3795
+ const icon = emoji[phaseName] || "\u{1F4CB}";
3796
+ return [
3797
+ t("notesync.phaseCompleted", { icon, label: phaseLabel }),
3798
+ "",
3799
+ summary,
3800
+ "",
3801
+ "---",
3802
+ t("notesync.viewDoc", { label: phaseLabel, url: docUrl }),
3803
+ t("notesync.viewDashboard", { url: dashboardUrl })
3804
+ ].join("\n");
3805
+ }
3806
+
3837
3807
  // src/orchestrator/steps/PhaseLoopStep.ts
3838
- var logger17 = logger.child("PhaseLoopStep");
3808
+ var logger15 = logger.child("PhaseLoopStep");
3839
3809
  function resolveVerifyRunner(deps) {
3840
3810
  return deps.aiRunner;
3841
3811
  }
@@ -3845,6 +3815,162 @@ function resolveUatRunner(deps, issueIid) {
3845
3815
  }
3846
3816
  return deps.aiRunner;
3847
3817
  }
3818
+ async function commitPlanFiles(ctx, wtGit, wtGitMap, phaseName, displayId) {
3819
+ const commitMsg = `chore(auto): ${phaseName} phase completed for issue #${displayId}`;
3820
+ if (ctx.workspace && ctx.workspace.repos.length > 1) {
3821
+ for (const repo of ctx.workspace.repos) {
3822
+ const repoGit = wtGitMap?.get(repo.name);
3823
+ if (!repoGit) {
3824
+ logger15.warn("Missing GitOperations for repo, skipping commit", { repo: repo.name });
3825
+ continue;
3826
+ }
3827
+ const branch = repo.branchPrefix ? `${repo.branchPrefix}-${displayId}` : ctx.branchName;
3828
+ try {
3829
+ if (await repoGit.hasChanges()) {
3830
+ await repoGit.add(["."]);
3831
+ await repoGit.commit(commitMsg);
3832
+ await repoGit.push(branch);
3833
+ logger15.info("Committed changes for repo", { repo: repo.name, branch });
3834
+ }
3835
+ } catch (err) {
3836
+ logger15.warn("Failed to commit/push for repo", {
3837
+ repo: repo.name,
3838
+ error: err.message
3839
+ });
3840
+ }
3841
+ }
3842
+ } else {
3843
+ if (await wtGit.hasChanges()) {
3844
+ await wtGit.add(["."]);
3845
+ await wtGit.commit(commitMsg);
3846
+ await wtGit.push(ctx.branchName);
3847
+ }
3848
+ }
3849
+ }
3850
+ async function syncResultToIssue(phase, ctx, displayId, phaseName, deps, issueId, wtPlan) {
3851
+ try {
3852
+ const enabled = isNoteSyncEnabledForIssue(displayId, deps.tracker, deps.config);
3853
+ const resultFiles = phase.getResultFiles(ctx);
3854
+ if (!enabled || resultFiles.length === 0) {
3855
+ await safeComment(deps, issueId, issueProgressComment(phaseName, "completed"));
3856
+ return;
3857
+ }
3858
+ const baseUrl = deps.config.issueNoteSync.webBaseUrl.replace(/\/$/, "");
3859
+ const phaseLabel = t(`phase.${phaseName}`) || phaseName;
3860
+ const dashboardUrl = `${baseUrl}/?issue=${displayId}`;
3861
+ for (const file of resultFiles) {
3862
+ const content = wtPlan.readFile(file.filename);
3863
+ if (!content) continue;
3864
+ const summary = truncateToSummary(content);
3865
+ const docUrl = `${baseUrl}/doc/${displayId}/${file.filename}`;
3866
+ const comment = buildNoteSyncComment(
3867
+ phaseName,
3868
+ file.label || phaseLabel,
3869
+ docUrl,
3870
+ dashboardUrl,
3871
+ summary
3872
+ );
3873
+ await safeComment(deps, issueId, comment);
3874
+ logger15.info("Result synced to issue", { issueIid: displayId, file: file.filename });
3875
+ }
3876
+ } catch (err) {
3877
+ logger15.warn("Failed to sync result to issue", { error: err.message });
3878
+ await safeComment(deps, issueId, issueProgressComment(phaseName, "completed"));
3879
+ }
3880
+ }
3881
+ function buildAcpHandler(displayId, phaseName, deps) {
3882
+ if (deps.config.ai.codebuddyAcpAutoApprove !== false) return void 0;
3883
+ return (request) => {
3884
+ if (request.type !== "plan-approval") {
3885
+ return Promise.resolve("allow");
3886
+ }
3887
+ logger15.info("ACP plan-approval requested, delegating to review gate", {
3888
+ issueIid: displayId,
3889
+ phase: phaseName
3890
+ });
3891
+ deps.eventBus.emitTyped("review:requested", { issueIid: displayId });
3892
+ return new Promise((resolve) => {
3893
+ const onApproved = (payload) => {
3894
+ const data = payload.data;
3895
+ if (data.issueIid !== displayId) return;
3896
+ cleanup();
3897
+ logger15.info("ACP plan-approval approved via review gate", { issueIid: displayId });
3898
+ resolve("allow");
3899
+ };
3900
+ const onRejected = (payload) => {
3901
+ const data = payload.data;
3902
+ if (data.issueIid !== displayId) return;
3903
+ cleanup();
3904
+ logger15.info("ACP plan-approval rejected via review gate", { issueIid: displayId });
3905
+ resolve("reject");
3906
+ };
3907
+ const cleanup = () => {
3908
+ deps.eventBus.removeListener("review:approved", onApproved);
3909
+ deps.eventBus.removeListener("review:rejected", onRejected);
3910
+ };
3911
+ deps.eventBus.on("review:approved", onApproved);
3912
+ deps.eventBus.on("review:rejected", onRejected);
3913
+ });
3914
+ };
3915
+ }
3916
+ async function safeComment(deps, issueId, message) {
3917
+ try {
3918
+ await deps.gongfeng.createIssueNote(issueId, message);
3919
+ } catch {
3920
+ }
3921
+ }
3922
+ async function runPhaseWithLifecycle(phase, phaseCtx, spec, ctx, deps, wtGit, wtPlan, wtGitMap) {
3923
+ const { issue } = ctx;
3924
+ const displayId = issue.iid;
3925
+ deps.tracker.updateState(displayId, spec.startState, { currentPhase: spec.name });
3926
+ deps.tracker.updatePhaseProgress(displayId, spec.name, {
3927
+ status: "in_progress",
3928
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
3929
+ });
3930
+ wtPlan.updatePhaseProgress(spec.name, "in_progress");
3931
+ await safeComment(deps, issue.id, issueProgressComment(spec.name, "in_progress"));
3932
+ const phaseLabel = t(`phase.${spec.name}`) || spec.name;
3933
+ deps.eventBus.emitTyped("agent:output", {
3934
+ issueIid: displayId,
3935
+ phase: spec.name,
3936
+ event: { type: "system", content: t("basePhase.aiStarting", { label: phaseLabel }), timestamp: (/* @__PURE__ */ new Date()).toISOString() }
3937
+ });
3938
+ const callbacks = {
3939
+ onStreamEvent: (event) => deps.eventBus.emitTyped("agent:output", {
3940
+ issueIid: displayId,
3941
+ phase: spec.name,
3942
+ event
3943
+ }),
3944
+ onInputRequired: buildAcpHandler(displayId, spec.name, deps)
3945
+ };
3946
+ const outcome = await phase.run(phaseCtx, callbacks);
3947
+ if (outcome.sessionId) wtPlan.updatePhaseSessionId(spec.name, outcome.sessionId);
3948
+ if (outcome.status === "completed") {
3949
+ deps.tracker.updateState(displayId, spec.doneState, { currentPhase: spec.name });
3950
+ deps.tracker.updatePhaseProgress(displayId, spec.name, {
3951
+ status: "completed",
3952
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
3953
+ });
3954
+ wtPlan.updatePhaseProgress(spec.name, "completed");
3955
+ await commitPlanFiles(phaseCtx, wtGit, wtGitMap, spec.name, displayId);
3956
+ await syncResultToIssue(phase, phaseCtx, displayId, spec.name, deps, issue.id, wtPlan);
3957
+ return outcome;
3958
+ }
3959
+ if (outcome.status === "running") {
3960
+ return outcome;
3961
+ }
3962
+ const errMsg = outcome.error?.message ?? "Unknown error";
3963
+ const shortErr = errMsg.slice(0, 200);
3964
+ wtPlan.updatePhaseProgress(spec.name, "failed", errMsg);
3965
+ deps.tracker.updatePhaseProgress(displayId, spec.name, { status: "failed" });
3966
+ deps.tracker.markFailed(displayId, errMsg, "phase_running" /* PhaseRunning */);
3967
+ await safeComment(deps, issue.id, issueProgressComment(spec.name, "failed", shortErr));
3968
+ throw new AIExecutionError(spec.name, `Phase ${spec.name} failed: ${shortErr}`, {
3969
+ output: outcome.error?.rawOutput ?? outcome.output,
3970
+ exitCode: outcome.exitCode ?? 1,
3971
+ isRetryable: outcome.error?.isRetryable
3972
+ });
3973
+ }
3848
3974
  async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3849
3975
  const { issue, pipelineDef, lifecycleManager, record, isRetry, phaseCtx } = ctx;
3850
3976
  const startIdx = lifecycleManager.determineResumePhaseIndex(
@@ -3866,15 +3992,15 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3866
3992
  if (skippedDeployPhase && !phaseCtx.ports) {
3867
3993
  const existingPorts = deps.getPortsForIssue(issue.iid);
3868
3994
  if (existingPorts && deps.isPreviewRunning(issue.iid)) {
3869
- logger17.info("Restored preview ports from allocator", { iid: issue.iid, ...existingPorts });
3995
+ logger15.info("Restored preview ports from allocator", { iid: issue.iid, ...existingPorts });
3870
3996
  phaseCtx.ports = existingPorts;
3871
3997
  ctx.wtCtx.ports = existingPorts;
3872
3998
  serversStarted = true;
3873
3999
  } else {
3874
4000
  if (existingPorts) {
3875
- logger17.info("Ports allocated but servers not running, restarting", { iid: issue.iid });
4001
+ logger15.info("Ports allocated but servers not running, restarting", { iid: issue.iid });
3876
4002
  } else {
3877
- logger17.info("Restarting preview servers for resumed pipeline", { iid: issue.iid });
4003
+ logger15.info("Restarting preview servers for resumed pipeline", { iid: issue.iid });
3878
4004
  }
3879
4005
  const ports = await deps.startPreviewServers(ctx.wtCtx, issue);
3880
4006
  if (ports) {
@@ -3885,6 +4011,44 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3885
4011
  }
3886
4012
  }
3887
4013
  }
4014
+ if (startIdx > 0) {
4015
+ const currentProgress = wtPlan.readProgress();
4016
+ if (currentProgress) {
4017
+ let patched = false;
4018
+ for (let i = 0; i < startIdx; i++) {
4019
+ const prevSpec = pipelineDef.phases[i];
4020
+ const pp = currentProgress.phases[prevSpec.name];
4021
+ if (pp && pp.status !== "completed") {
4022
+ logger15.warn("Fixing stale phase progress", {
4023
+ iid: issue.iid,
4024
+ phase: prevSpec.name,
4025
+ was: pp.status,
4026
+ now: "completed"
4027
+ });
4028
+ pp.status = "completed";
4029
+ if (!pp.completedAt) {
4030
+ pp.completedAt = (/* @__PURE__ */ new Date()).toISOString();
4031
+ }
4032
+ patched = true;
4033
+ }
4034
+ }
4035
+ if (patched) {
4036
+ wtPlan.writeProgress(currentProgress);
4037
+ }
4038
+ }
4039
+ if (record.phaseProgress) {
4040
+ for (let i = 0; i < startIdx; i++) {
4041
+ const prevSpec = pipelineDef.phases[i];
4042
+ const tp = record.phaseProgress[prevSpec.name];
4043
+ if (tp && tp.status !== "completed") {
4044
+ deps.tracker.updatePhaseProgress(issue.iid, prevSpec.name, {
4045
+ status: "completed",
4046
+ completedAt: tp.completedAt ?? (/* @__PURE__ */ new Date()).toISOString()
4047
+ });
4048
+ }
4049
+ }
4050
+ }
4051
+ }
3888
4052
  for (let i = startIdx; i < pipelineDef.phases.length; i++) {
3889
4053
  if (isShuttingDown()) {
3890
4054
  throw new ServiceShutdownError();
@@ -3896,7 +4060,7 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3896
4060
  }
3897
4061
  if (spec.kind === "gate") {
3898
4062
  if (deps.shouldAutoApprove(issue.labels)) {
3899
- logger17.info("Auto-approving review gate (matched autoApproveLabels)", {
4063
+ logger15.info("Auto-approving review gate (matched autoApproveLabels)", {
3900
4064
  iid: issue.iid,
3901
4065
  labels: issue.labels,
3902
4066
  autoApproveLabels: deps.config.review.autoApproveLabels
@@ -3905,6 +4069,10 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3905
4069
  deps.tracker.updateState(issue.iid, spec.approvedState, { currentPhase: spec.name });
3906
4070
  }
3907
4071
  wtPlan.updatePhaseProgress(spec.name, "completed");
4072
+ deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
4073
+ status: "completed",
4074
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
4075
+ });
3908
4076
  try {
3909
4077
  await deps.gongfeng.createIssueNote(
3910
4078
  issue.id,
@@ -3916,8 +4084,12 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3916
4084
  }
3917
4085
  deps.tracker.updateState(issue.iid, spec.startState, { currentPhase: spec.name });
3918
4086
  wtPlan.updatePhaseProgress(spec.name, "in_progress");
4087
+ deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
4088
+ status: "in_progress",
4089
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
4090
+ });
3919
4091
  deps.eventBus.emitTyped("review:requested", { issueIid: issue.iid });
3920
- logger17.info("Review gate reached, pausing", { iid: issue.iid });
4092
+ logger15.info("Review gate reached, pausing", { iid: issue.iid });
3921
4093
  return { serversStarted, paused: true };
3922
4094
  }
3923
4095
  if (spec.name === "verify" && deps.config.verifyFixLoop.enabled) {
@@ -3926,28 +4098,56 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3926
4098
  continue;
3927
4099
  }
3928
4100
  if (spec.name === "uat" && !isE2eEnabledForIssue(issue.iid, deps.tracker, deps.config)) {
3929
- logger17.info("UAT phase skipped (E2E not enabled for this issue)", { iid: issue.iid });
4101
+ logger15.info("UAT phase skipped (E2E not enabled for this issue)", { iid: issue.iid });
3930
4102
  deps.tracker.updateState(issue.iid, spec.doneState, { currentPhase: spec.name });
3931
4103
  wtPlan.updatePhaseProgress(spec.name, "completed");
4104
+ deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
4105
+ status: "completed",
4106
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
4107
+ });
3932
4108
  continue;
3933
4109
  }
3934
- const hooks = deps.buildPhaseHooks(issue.iid, issue.id);
3935
4110
  const runner = spec.name === "verify" ? resolveVerifyRunner(deps) : spec.name === "uat" ? resolveUatRunner(deps, issue.iid) : deps.aiRunner;
3936
4111
  if (spec.name === "uat") {
3937
4112
  const runnerName = runner === deps.e2eAiRunner ? "e2eAiRunner (CodeBuddy)" : "mainRunner";
3938
- logger17.info("UAT phase starting", { iid: issue.iid, runner: runnerName });
4113
+ logger15.info("UAT phase starting", { iid: issue.iid, runner: runnerName });
3939
4114
  }
3940
- const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config, lifecycleManager, hooks);
3941
- if (wtGitMap && wtGitMap.size > 1) {
4115
+ const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config);
4116
+ if (wtGitMap) {
3942
4117
  phase.setWtGitMap(wtGitMap);
3943
4118
  }
3944
- const result = await phase.execute(phaseCtx);
3945
- if (spec.approvedState && result && "gateRequested" in result && result.gateRequested) {
4119
+ const outcome = await runPhaseWithLifecycle(
4120
+ phase,
4121
+ phaseCtx,
4122
+ spec,
4123
+ ctx,
4124
+ deps,
4125
+ wtGit,
4126
+ wtPlan,
4127
+ wtGitMap
4128
+ );
4129
+ if (outcome.status === "running") {
4130
+ if (outcome.awaitCompletion) {
4131
+ logger15.info("Async phase running, awaiting completion", { iid: issue.iid, phase: spec.name });
4132
+ const finalOutcome = await awaitAsyncPhase(outcome, spec, ctx, deps, wtGit, wtPlan, wtGitMap);
4133
+ if (finalOutcome.status === "completed") {
4134
+ continue;
4135
+ }
4136
+ }
3946
4137
  deps.tracker.updateState(issue.iid, "phase_waiting" /* PhaseWaiting */, { currentPhase: spec.name });
3947
4138
  wtPlan.updatePhaseProgress(spec.name, "gate_waiting");
4139
+ deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "gate_waiting" });
3948
4140
  const gateEvent = spec.name === "uat" ? "uat:gateRequested" : "release:gateRequested";
3949
4141
  deps.eventBus.emitTyped(gateEvent, { issueIid: issue.iid });
3950
- logger17.info("AI phase requested gate, pausing", { iid: issue.iid, phase: spec.name });
4142
+ logger15.info("Async phase running (no awaitCompletion), pausing pipeline", { iid: issue.iid, phase: spec.name });
4143
+ return { serversStarted, paused: true };
4144
+ }
4145
+ if (spec.approvedState && outcome.data?.hasReleaseCapability) {
4146
+ deps.tracker.updateState(issue.iid, "phase_waiting" /* PhaseWaiting */, { currentPhase: spec.name });
4147
+ wtPlan.updatePhaseProgress(spec.name, "gate_waiting");
4148
+ deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "gate_waiting" });
4149
+ deps.eventBus.emitTyped("release:gateRequested", { issueIid: issue.iid });
4150
+ logger15.info("Phase requested gate, pausing", { iid: issue.iid, phase: spec.name });
3951
4151
  return { serversStarted, paused: true };
3952
4152
  }
3953
4153
  if (needsDeployment && !serversStarted && lifecycleManager.shouldDeployPreview(spec.name)) {
@@ -3961,6 +4161,40 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
3961
4161
  }
3962
4162
  return { serversStarted, paused: false };
3963
4163
  }
4164
+ async function awaitAsyncPhase(outcome, spec, ctx, deps, wtGit, wtPlan, wtGitMap) {
4165
+ const { issue } = ctx;
4166
+ const displayId = issue.iid;
4167
+ const phaseCtx = ctx.phaseCtx;
4168
+ const finalOutcome = await outcome.awaitCompletion;
4169
+ if (finalOutcome.sessionId) {
4170
+ wtPlan.updatePhaseSessionId(spec.name, finalOutcome.sessionId);
4171
+ }
4172
+ if (finalOutcome.status === "completed") {
4173
+ deps.tracker.updateState(displayId, spec.doneState, { currentPhase: spec.name });
4174
+ deps.tracker.updatePhaseProgress(displayId, spec.name, {
4175
+ status: "completed",
4176
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
4177
+ });
4178
+ wtPlan.updatePhaseProgress(spec.name, "completed");
4179
+ await commitPlanFiles(phaseCtx, wtGit, wtGitMap, spec.name, displayId);
4180
+ const runner = spec.name === "uat" ? resolveUatRunner(deps, displayId) : deps.aiRunner;
4181
+ const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config);
4182
+ await syncResultToIssue(phase, phaseCtx, displayId, spec.name, deps, issue.id, wtPlan);
4183
+ logger15.info("Async phase completed successfully", { iid: displayId, phase: spec.name });
4184
+ return finalOutcome;
4185
+ }
4186
+ const errMsg = finalOutcome.error?.message ?? "Unknown error";
4187
+ const shortErr = errMsg.slice(0, 200);
4188
+ wtPlan.updatePhaseProgress(spec.name, "failed", errMsg);
4189
+ deps.tracker.updatePhaseProgress(displayId, spec.name, { status: "failed" });
4190
+ deps.tracker.markFailed(displayId, errMsg, "phase_running" /* PhaseRunning */);
4191
+ await safeComment(deps, issue.id, issueProgressComment(spec.name, "failed", shortErr));
4192
+ throw new AIExecutionError(spec.name, `Phase ${spec.name} failed: ${shortErr}`, {
4193
+ output: finalOutcome.error?.rawOutput ?? finalOutcome.output,
4194
+ exitCode: finalOutcome.exitCode ?? 1,
4195
+ isRetryable: finalOutcome.error?.isRetryable
4196
+ });
4197
+ }
3964
4198
  function findPreviousAiPhaseIndex(phases, currentIdx) {
3965
4199
  for (let j = currentIdx - 1; j >= 0; j--) {
3966
4200
  if (phases[j].kind === "ai") return j;
@@ -3970,11 +4204,12 @@ function findPreviousAiPhaseIndex(phases, currentIdx) {
3970
4204
  async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, buildPhaseIdx, wtGitMap) {
3971
4205
  const { issue, lifecycleManager, phaseCtx } = ctx;
3972
4206
  const maxIterations = deps.config.verifyFixLoop.maxIterations;
4207
+ const verifySpec = ctx.pipelineDef.phases[verifyPhaseIdx];
3973
4208
  deps.eventBus.emitTyped("verify:loopStarted", {
3974
4209
  issueIid: issue.iid,
3975
4210
  maxIterations
3976
4211
  });
3977
- logger17.info("Verify-fix loop started", {
4212
+ logger15.info("Verify-fix loop started", {
3978
4213
  iid: issue.iid,
3979
4214
  maxIterations,
3980
4215
  buildPhaseIdx
@@ -3983,30 +4218,30 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
3983
4218
  if (isShuttingDown()) {
3984
4219
  throw new ServiceShutdownError();
3985
4220
  }
3986
- logger17.info("Verify-fix loop iteration", {
4221
+ logger15.info("Verify-fix loop iteration", {
3987
4222
  iteration,
3988
4223
  maxIterations,
3989
4224
  iid: issue.iid
3990
4225
  });
3991
- const verifyHooks = deps.buildPhaseHooks(issue.iid, issue.id);
3992
4226
  const verifyRunner = resolveVerifyRunner(deps);
3993
- const verifyPhase = createPhase(
3994
- "verify",
3995
- verifyRunner,
3996
- wtGit,
3997
- wtPlan,
3998
- deps.config,
3999
- lifecycleManager,
4000
- verifyHooks
4001
- );
4002
- if (wtGitMap && wtGitMap.size > 1) {
4227
+ const verifyPhase = createPhase("verify", verifyRunner, wtGit, wtPlan, deps.config);
4228
+ if (wtGitMap) {
4003
4229
  verifyPhase.setWtGitMap(wtGitMap);
4004
4230
  }
4005
- let verifyResult;
4231
+ let verifyOutcome;
4006
4232
  try {
4007
- verifyResult = await verifyPhase.execute(phaseCtx);
4233
+ verifyOutcome = await runPhaseWithLifecycle(
4234
+ verifyPhase,
4235
+ phaseCtx,
4236
+ verifySpec,
4237
+ ctx,
4238
+ deps,
4239
+ wtGit,
4240
+ wtPlan,
4241
+ wtGitMap
4242
+ );
4008
4243
  } catch (err) {
4009
- logger17.warn("Verify phase execution failed", {
4244
+ logger15.warn("Verify phase execution failed", {
4010
4245
  iteration,
4011
4246
  iid: issue.iid,
4012
4247
  error: err.message
@@ -4029,7 +4264,7 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
4029
4264
  }
4030
4265
  continue;
4031
4266
  }
4032
- const report = verifyResult.verifyReport;
4267
+ const report = verifyOutcome.data?.verifyReport;
4033
4268
  const passed = report ? report.passed : true;
4034
4269
  deps.eventBus.emitTyped("verify:iterationComplete", {
4035
4270
  issueIid: issue.iid,
@@ -4038,13 +4273,13 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
4038
4273
  failures: report?.failureReasons
4039
4274
  });
4040
4275
  if (passed) {
4041
- logger17.info("Verify-fix loop passed", {
4276
+ logger15.info("Verify-fix loop passed", {
4042
4277
  iteration,
4043
4278
  iid: issue.iid
4044
4279
  });
4045
4280
  return;
4046
4281
  }
4047
- logger17.info("Verify failed, issues found", {
4282
+ logger15.info("Verify failed, issues found", {
4048
4283
  iteration,
4049
4284
  iid: issue.iid,
4050
4285
  failures: report?.failureReasons,
@@ -4057,7 +4292,7 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
4057
4292
  failures: report?.failureReasons ?? []
4058
4293
  });
4059
4294
  const failMsg = `Verify-fix loop exhausted after ${maxIterations} iterations. Remaining issues: ${report?.failureReasons?.join("; ") ?? "unknown"}`;
4060
- logger17.warn(failMsg, { iid: issue.iid });
4295
+ logger15.warn(failMsg, { iid: issue.iid });
4061
4296
  throw new AIExecutionError("verify", failMsg, {
4062
4297
  output: report?.rawReport ?? "",
4063
4298
  exitCode: 0
@@ -4073,35 +4308,36 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
4073
4308
  }
4074
4309
  }
4075
4310
  async function executeBuildFix(ctx, deps, wtGit, wtPlan, buildPhaseIdx, fixContext, wtGitMap) {
4076
- const { issue, lifecycleManager, phaseCtx } = ctx;
4077
- logger17.info("Looping back to build for fix", {
4311
+ const { issue, phaseCtx } = ctx;
4312
+ const buildSpec = ctx.pipelineDef.phases[buildPhaseIdx];
4313
+ logger15.info("Looping back to build for fix", {
4078
4314
  iteration: fixContext.iteration,
4079
4315
  iid: issue.iid,
4080
4316
  failures: fixContext.verifyFailures
4081
4317
  });
4082
4318
  phaseCtx.fixContext = fixContext;
4083
4319
  try {
4084
- const buildHooks = deps.buildPhaseHooks(issue.iid, issue.id);
4085
- const buildPhase = createPhase(
4086
- "build",
4087
- deps.aiRunner,
4320
+ const buildPhase = createPhase("build", deps.aiRunner, wtGit, wtPlan, deps.config);
4321
+ if (wtGitMap) {
4322
+ buildPhase.setWtGitMap(wtGitMap);
4323
+ }
4324
+ await runPhaseWithLifecycle(
4325
+ buildPhase,
4326
+ phaseCtx,
4327
+ buildSpec,
4328
+ ctx,
4329
+ deps,
4088
4330
  wtGit,
4089
4331
  wtPlan,
4090
- deps.config,
4091
- lifecycleManager,
4092
- buildHooks
4332
+ wtGitMap
4093
4333
  );
4094
- if (wtGitMap && wtGitMap.size > 1) {
4095
- buildPhase.setWtGitMap(wtGitMap);
4096
- }
4097
- await buildPhase.execute(phaseCtx);
4098
4334
  } finally {
4099
4335
  delete phaseCtx.fixContext;
4100
4336
  }
4101
4337
  }
4102
4338
 
4103
4339
  // src/orchestrator/steps/CompletionStep.ts
4104
- var logger18 = logger.child("CompletionStep");
4340
+ var logger16 = logger.child("CompletionStep");
4105
4341
  async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
4106
4342
  const { issue, branchName, wtCtx } = ctx;
4107
4343
  deps.emitProgress(issue.iid, "create_mr", t("orchestrator.createMrProgress"));
@@ -4133,7 +4369,7 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
4133
4369
  mrIid: void 0
4134
4370
  });
4135
4371
  } catch (err) {
4136
- logger18.warn("Failed to publish E2E screenshots", {
4372
+ logger16.warn("Failed to publish E2E screenshots", {
4137
4373
  iid: issue.iid,
4138
4374
  error: err.message
4139
4375
  });
@@ -4153,19 +4389,19 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
4153
4389
  await deps.claimer.releaseClaim(issue.id, issue.iid, "completed");
4154
4390
  }
4155
4391
  if (phaseResult.serversStarted && deps.config.preview.keepAfterComplete) {
4156
- logger18.info("Preview servers kept running after completion", { iid: issue.iid });
4392
+ logger16.info("Preview servers kept running after completion", { iid: issue.iid });
4157
4393
  } else {
4158
4394
  deps.stopPreviewServers(issue.iid);
4159
4395
  await deps.mainGitMutex.runExclusive(async () => {
4160
4396
  if (wtCtx.workspace) {
4161
4397
  await deps.workspaceManager.cleanupWorkspace(wtCtx.workspace);
4162
- logger18.info("Workspace cleaned up", { dir: wtCtx.workspace.workspaceRoot });
4398
+ logger16.info("Workspace cleaned up", { dir: wtCtx.workspace.workspaceRoot });
4163
4399
  } else {
4164
4400
  try {
4165
4401
  await deps.mainGit.worktreeRemove(wtCtx.gitRootDir, true);
4166
- logger18.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
4402
+ logger16.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
4167
4403
  } catch (err) {
4168
- logger18.warn("Failed to cleanup worktree", {
4404
+ logger16.warn("Failed to cleanup worktree", {
4169
4405
  dir: wtCtx.gitRootDir,
4170
4406
  error: err.message
4171
4407
  });
@@ -4173,15 +4409,15 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
4173
4409
  }
4174
4410
  });
4175
4411
  }
4176
- logger18.info("Issue processing completed", { iid: issue.iid });
4412
+ logger16.info("Issue processing completed", { iid: issue.iid });
4177
4413
  }
4178
4414
 
4179
4415
  // src/orchestrator/steps/FailureHandler.ts
4180
- var logger19 = logger.child("FailureHandler");
4416
+ var logger17 = logger.child("FailureHandler");
4181
4417
  async function handleFailure(err, issue, wtCtx, deps) {
4182
4418
  const errorMsg = err.message;
4183
4419
  const isRetryable = err instanceof AIExecutionError ? err.isRetryable : true;
4184
- logger19.error("Issue processing failed", { iid: issue.iid, error: errorMsg, isRetryable });
4420
+ logger17.error("Issue processing failed", { iid: issue.iid, error: errorMsg, isRetryable });
4185
4421
  metrics.incCounter("iaf_issues_failed_total");
4186
4422
  const currentRecord = deps.tracker.get(issue.iid);
4187
4423
  const failedAtState = currentRecord?.state || "pending" /* Pending */;
@@ -4190,11 +4426,11 @@ async function handleFailure(err, issue, wtCtx, deps) {
4190
4426
  deps.tracker.markFailed(issue.iid, errorMsg.slice(0, 500), failedAtState, isRetryable);
4191
4427
  }
4192
4428
  if (wasReset) {
4193
- logger19.info("Issue was reset during processing, skipping failure marking", { iid: issue.iid });
4429
+ logger17.info("Issue was reset during processing, skipping failure marking", { iid: issue.iid });
4194
4430
  throw err;
4195
4431
  }
4196
4432
  if (failedAtState === "paused" /* Paused */) {
4197
- logger19.info("Issue was paused during processing, skipping failure handling", { iid: issue.iid });
4433
+ logger17.info("Issue was paused during processing, skipping failure handling", { iid: issue.iid });
4198
4434
  throw err;
4199
4435
  }
4200
4436
  try {
@@ -4216,7 +4452,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
4216
4452
  try {
4217
4453
  await deps.claimer.releaseClaim(issue.id, issue.iid, "failed");
4218
4454
  } catch (releaseErr) {
4219
- logger19.warn("Failed to release lock on failure", {
4455
+ logger17.warn("Failed to release lock on failure", {
4220
4456
  iid: issue.iid,
4221
4457
  error: releaseErr.message
4222
4458
  });
@@ -4224,7 +4460,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
4224
4460
  }
4225
4461
  deps.stopPreviewServers(issue.iid);
4226
4462
  const preservedDirs = wtCtx.workspace ? [wtCtx.workspace.primary.gitRootDir, ...wtCtx.workspace.associates.map((a) => a.gitRootDir)] : [wtCtx.gitRootDir];
4227
- logger19.info("Worktree(s) preserved for debugging", {
4463
+ logger17.info("Worktree(s) preserved for debugging", {
4228
4464
  primary: wtCtx.gitRootDir,
4229
4465
  all: preservedDirs
4230
4466
  });
@@ -4233,7 +4469,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
4233
4469
 
4234
4470
  // src/orchestrator/PipelineOrchestrator.ts
4235
4471
  var execFileAsync2 = promisify2(execFile2);
4236
- var logger20 = logger.child("PipelineOrchestrator");
4472
+ var logger18 = logger.child("PipelineOrchestrator");
4237
4473
  var PipelineOrchestrator = class {
4238
4474
  config;
4239
4475
  gongfeng;
@@ -4263,7 +4499,7 @@ var PipelineOrchestrator = class {
4263
4499
  setAIRunner(runner) {
4264
4500
  this.aiRunner = runner;
4265
4501
  this.conflictResolver = new ConflictResolver(runner);
4266
- logger20.info("AIRunner replaced via hot-reload");
4502
+ logger18.info("AIRunner replaced via hot-reload");
4267
4503
  }
4268
4504
  constructor(config, gongfeng, git, aiRunner, tracker, supplementStore, mainGitMutex, eventBusInstance, wsConfig, tenantId, e2eAiRunner) {
4269
4505
  this.config = config;
@@ -4281,14 +4517,14 @@ var PipelineOrchestrator = class {
4281
4517
  this.pipelineDef = mode === "plan-mode" ? buildPlanModePipeline({ releaseEnabled: config.release.enabled, e2eEnabled: config.e2e.enabled }) : getPipelineDef(mode);
4282
4518
  registerPipeline(this.pipelineDef);
4283
4519
  this.lifecycleManager = createLifecycleManager(this.pipelineDef);
4284
- logger20.info("Pipeline mode resolved", { tenantId: this.tenantId, mode: this.pipelineDef.mode, aiMode: config.ai.mode });
4520
+ logger18.info("Pipeline mode resolved", { tenantId: this.tenantId, mode: this.pipelineDef.mode, aiMode: config.ai.mode });
4285
4521
  this.portAllocator = new PortAllocator({
4286
4522
  backendPortBase: config.e2e.backendPortBase,
4287
4523
  frontendPortBase: config.e2e.frontendPortBase
4288
4524
  });
4289
4525
  this.devServerManager = new DevServerManager();
4290
4526
  this.screenshotPublisher = new ScreenshotPublisher(gongfeng);
4291
- this.effectiveWorktreeBaseDir = this.tenantId === "default" ? config.project.worktreeBaseDir : path14.join(config.project.worktreeBaseDir, this.tenantId);
4527
+ this.effectiveWorktreeBaseDir = this.tenantId === "default" ? config.project.worktreeBaseDir : path11.join(config.project.worktreeBaseDir, this.tenantId);
4292
4528
  const effectiveWsConfig = wsConfig ?? buildSingleRepoWorkspace(config.project, config.gongfeng.projectPath);
4293
4529
  this.workspaceManager = new WorkspaceManager({
4294
4530
  wsConfig: effectiveWsConfig,
@@ -4297,7 +4533,7 @@ var PipelineOrchestrator = class {
4297
4533
  mainGitMutex: this.mainGitMutex,
4298
4534
  gongfengApiUrl: config.gongfeng.apiUrl
4299
4535
  });
4300
- logger20.info("WorkspaceManager initialized", {
4536
+ logger18.info("WorkspaceManager initialized", {
4301
4537
  tenantId: this.tenantId,
4302
4538
  primary: effectiveWsConfig.primary.name,
4303
4539
  associates: effectiveWsConfig.associates.map((a) => a.name)
@@ -4318,7 +4554,7 @@ var PipelineOrchestrator = class {
4318
4554
  this.claimer = claimer;
4319
4555
  }
4320
4556
  async cleanupStaleState() {
4321
- logger20.info("Cleaning up stale worktree state...");
4557
+ logger18.info("Cleaning up stale worktree state...");
4322
4558
  let cleaned = 0;
4323
4559
  const repoGitRoot = this.config.project.gitRootDir;
4324
4560
  try {
@@ -4327,11 +4563,11 @@ var PipelineOrchestrator = class {
4327
4563
  if (wtDir === repoGitRoot) continue;
4328
4564
  if (!wtDir.includes("/issue-")) continue;
4329
4565
  try {
4330
- const gitFile = path14.join(wtDir, ".git");
4566
+ const gitFile = path11.join(wtDir, ".git");
4331
4567
  try {
4332
- await fs13.access(gitFile);
4568
+ await fs9.access(gitFile);
4333
4569
  } catch {
4334
- logger20.warn("Worktree corrupted (.git missing), force removing", { dir: wtDir });
4570
+ logger18.warn("Worktree corrupted (.git missing), force removing", { dir: wtDir });
4335
4571
  await this.mainGit.worktreeRemove(wtDir, true).catch(() => {
4336
4572
  });
4337
4573
  await this.mainGit.worktreePrune();
@@ -4340,32 +4576,32 @@ var PipelineOrchestrator = class {
4340
4576
  }
4341
4577
  const wtGit = new GitOperations(wtDir);
4342
4578
  if (await wtGit.isRebaseInProgress()) {
4343
- logger20.warn("Aborting residual rebase in worktree", { dir: wtDir });
4579
+ logger18.warn("Aborting residual rebase in worktree", { dir: wtDir });
4344
4580
  await wtGit.rebaseAbort();
4345
4581
  cleaned++;
4346
4582
  }
4347
- const indexLock = path14.join(wtDir, ".git", "index.lock");
4583
+ const indexLock = path11.join(wtDir, ".git", "index.lock");
4348
4584
  try {
4349
- await fs13.unlink(indexLock);
4350
- logger20.warn("Removed stale index.lock", { path: indexLock });
4585
+ await fs9.unlink(indexLock);
4586
+ logger18.warn("Removed stale index.lock", { path: indexLock });
4351
4587
  cleaned++;
4352
4588
  } catch {
4353
4589
  }
4354
4590
  } catch (err) {
4355
- logger20.warn("Failed to clean worktree state", { dir: wtDir, error: err.message });
4591
+ logger18.warn("Failed to clean worktree state", { dir: wtDir, error: err.message });
4356
4592
  }
4357
4593
  }
4358
4594
  } catch (err) {
4359
- logger20.warn("Failed to list worktrees for cleanup", { error: err.message });
4595
+ logger18.warn("Failed to list worktrees for cleanup", { error: err.message });
4360
4596
  }
4361
- const mainIndexLock = path14.join(repoGitRoot, ".git", "index.lock");
4597
+ const mainIndexLock = path11.join(repoGitRoot, ".git", "index.lock");
4362
4598
  try {
4363
- await fs13.unlink(mainIndexLock);
4364
- logger20.warn("Removed stale main repo index.lock", { path: mainIndexLock });
4599
+ await fs9.unlink(mainIndexLock);
4600
+ logger18.warn("Removed stale main repo index.lock", { path: mainIndexLock });
4365
4601
  cleaned++;
4366
4602
  } catch {
4367
4603
  }
4368
- logger20.info("Stale state cleanup complete", { cleaned });
4604
+ logger18.info("Stale state cleanup complete", { cleaned });
4369
4605
  }
4370
4606
  /**
4371
4607
  * 重启后清理幽灵端口分配。
@@ -4378,7 +4614,7 @@ var PipelineOrchestrator = class {
4378
4614
  for (const record of this.tracker.getAll()) {
4379
4615
  if (record.ports) {
4380
4616
  const iid = getIid(record);
4381
- logger20.info("Clearing stale port allocation after restart", { iid, ports: record.ports });
4617
+ logger18.info("Clearing stale port allocation after restart", { iid, ports: record.ports });
4382
4618
  this.tracker.updateState(iid, record.state, {
4383
4619
  ports: void 0,
4384
4620
  previewStartedAt: void 0
@@ -4423,20 +4659,20 @@ var PipelineOrchestrator = class {
4423
4659
  }
4424
4660
  try {
4425
4661
  await this.mainGit.worktreeRemove(wtCtx.gitRootDir, true);
4426
- logger20.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
4662
+ logger18.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
4427
4663
  } catch (err) {
4428
- logger20.warn("Failed to cleanup worktree", { dir: wtCtx.gitRootDir, error: err.message });
4664
+ logger18.warn("Failed to cleanup worktree", { dir: wtCtx.gitRootDir, error: err.message });
4429
4665
  }
4430
4666
  }
4431
4667
  async installDependencies(workDir) {
4432
- logger20.info("Installing dependencies in worktree", { workDir });
4668
+ logger18.info("Installing dependencies in worktree", { workDir });
4433
4669
  const knowledge = getProjectKnowledge() ?? KNOWLEDGE_DEFAULTS;
4434
4670
  const pkgMgr = knowledge.toolchain.packageManager.toLowerCase();
4435
4671
  const isNodeProject = ["npm", "pnpm", "yarn", "bun"].some((m) => pkgMgr.includes(m));
4436
4672
  if (isNodeProject) {
4437
4673
  const ready = await this.ensureNodeModules(workDir);
4438
4674
  if (ready) {
4439
- logger20.info("node_modules ready \u2014 skipping install");
4675
+ logger18.info("node_modules ready \u2014 skipping install");
4440
4676
  return;
4441
4677
  }
4442
4678
  }
@@ -4449,10 +4685,10 @@ var PipelineOrchestrator = class {
4449
4685
  maxBuffer: 10 * 1024 * 1024,
4450
4686
  timeout: 3e5
4451
4687
  });
4452
- logger20.info("Dependencies installed");
4688
+ logger18.info("Dependencies installed");
4453
4689
  } catch (err) {
4454
4690
  if (fallbackCmd) {
4455
- logger20.warn(`${installCmd} failed, retrying with fallback command`, {
4691
+ logger18.warn(`${installCmd} failed, retrying with fallback command`, {
4456
4692
  error: err.message
4457
4693
  });
4458
4694
  const [fallbackBin, ...fallbackArgs] = fallbackCmd.split(/\s+/);
@@ -4462,45 +4698,45 @@ var PipelineOrchestrator = class {
4462
4698
  maxBuffer: 10 * 1024 * 1024,
4463
4699
  timeout: 3e5
4464
4700
  });
4465
- logger20.info("Dependencies installed (fallback)");
4701
+ logger18.info("Dependencies installed (fallback)");
4466
4702
  } catch (retryErr) {
4467
- logger20.warn("Fallback install also failed", {
4703
+ logger18.warn("Fallback install also failed", {
4468
4704
  error: retryErr.message
4469
4705
  });
4470
4706
  }
4471
4707
  } else {
4472
- logger20.warn("Install failed, no fallback configured", {
4708
+ logger18.warn("Install failed, no fallback configured", {
4473
4709
  error: err.message
4474
4710
  });
4475
4711
  }
4476
4712
  }
4477
4713
  }
4478
4714
  async ensureNodeModules(workDir) {
4479
- const targetBin = path14.join(workDir, "node_modules", ".bin");
4715
+ const targetBin = path11.join(workDir, "node_modules", ".bin");
4480
4716
  try {
4481
- await fs13.access(targetBin);
4482
- logger20.info("node_modules already complete (has .bin/)");
4717
+ await fs9.access(targetBin);
4718
+ logger18.info("node_modules already complete (has .bin/)");
4483
4719
  return true;
4484
4720
  } catch {
4485
4721
  }
4486
- const sourceNM = path14.join(this.config.project.workDir, "node_modules");
4487
- const targetNM = path14.join(workDir, "node_modules");
4722
+ const sourceNM = path11.join(this.config.project.workDir, "node_modules");
4723
+ const targetNM = path11.join(workDir, "node_modules");
4488
4724
  try {
4489
- await fs13.access(sourceNM);
4725
+ await fs9.access(sourceNM);
4490
4726
  } catch {
4491
- logger20.warn("Main repo node_modules not found, skipping seed", { sourceNM });
4727
+ logger18.warn("Main repo node_modules not found, skipping seed", { sourceNM });
4492
4728
  return false;
4493
4729
  }
4494
- logger20.info("Seeding node_modules from main repo via reflink copy", { sourceNM, targetNM });
4730
+ logger18.info("Seeding node_modules from main repo via reflink copy", { sourceNM, targetNM });
4495
4731
  try {
4496
4732
  await execFileAsync2("rm", ["-rf", targetNM], { timeout: 6e4 });
4497
4733
  await execFileAsync2("cp", ["-a", "--reflink=auto", sourceNM, targetNM], {
4498
4734
  timeout: 12e4
4499
4735
  });
4500
- logger20.info("node_modules seeded from main repo");
4736
+ logger18.info("node_modules seeded from main repo");
4501
4737
  return true;
4502
4738
  } catch (err) {
4503
- logger20.warn("Failed to seed node_modules from main repo", {
4739
+ logger18.warn("Failed to seed node_modules from main repo", {
4504
4740
  error: err.message
4505
4741
  });
4506
4742
  return false;
@@ -4510,17 +4746,20 @@ var PipelineOrchestrator = class {
4510
4746
  const record = this.tracker.get(issueIid);
4511
4747
  if (!record) throw new IssueNotFoundError(issueIid);
4512
4748
  const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
4513
- logger20.info("Restarting issue \u2014 cleaning context", { issueIid, branchName: record.branchName });
4749
+ logger18.info("Restarting issue \u2014 cleaning context", { issueIid, branchName: record.branchName });
4750
+ this.pendingActions.set(issueIid, "restart");
4514
4751
  this.aiRunner.killByWorkDir(wtCtx.workDir);
4752
+ this.e2eAiRunner?.killByWorkDir(wtCtx.workDir);
4515
4753
  this.stopPreviewServers(issueIid);
4516
4754
  try {
4517
4755
  const deleted = await this.gongfeng.cleanupAgentNotes(getExternalId(record));
4518
- logger20.info("Agent notes cleaned up", { issueIid, deleted });
4756
+ logger18.info("Agent notes cleaned up", { issueIid, deleted });
4519
4757
  } catch (err) {
4520
- logger20.warn("Failed to cleanup agent notes", { issueIid, error: err.message });
4758
+ logger18.warn("Failed to cleanup agent notes", { issueIid, error: err.message });
4521
4759
  }
4522
4760
  await this.mainGitMutex.runExclusive(async () => {
4523
4761
  await this.cleanupWorktree(wtCtx);
4762
+ await this.cleanupWorkspaceRoot(issueIid);
4524
4763
  try {
4525
4764
  await this.mainGit.deleteBranch(record.branchName);
4526
4765
  } catch {
@@ -4530,23 +4769,26 @@ var PipelineOrchestrator = class {
4530
4769
  } catch {
4531
4770
  }
4532
4771
  });
4772
+ await this.cleanupE2eOutputs(issueIid);
4533
4773
  this.tracker.resetFull(issueIid);
4534
- logger20.info("Issue restarted", { issueIid });
4774
+ this.pendingActions.delete(issueIid);
4775
+ logger18.info("Issue restarted", { issueIid });
4535
4776
  }
4536
4777
  async cancelIssue(issueIid) {
4537
4778
  const record = this.tracker.get(issueIid);
4538
4779
  if (!record) throw new IssueNotFoundError(issueIid);
4539
4780
  const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
4540
- logger20.info("Cancelling issue \u2014 cleaning all resources", { issueIid, branchName: record.branchName });
4781
+ logger18.info("Cancelling issue \u2014 cleaning all resources", { issueIid, branchName: record.branchName });
4541
4782
  this.aiRunner.killByWorkDir(wtCtx.workDir);
4542
4783
  this.stopPreviewServers(issueIid);
4543
4784
  try {
4544
4785
  await this.gongfeng.removeLabelsWithPrefix(getExternalId(record), "auto-finish");
4545
4786
  } catch (err) {
4546
- logger20.warn("Failed to remove labels on cancel", { issueIid, error: err.message });
4787
+ logger18.warn("Failed to remove labels on cancel", { issueIid, error: err.message });
4547
4788
  }
4548
4789
  await this.mainGitMutex.runExclusive(async () => {
4549
4790
  await this.cleanupWorktree(wtCtx);
4791
+ await this.cleanupWorkspaceRoot(issueIid);
4550
4792
  try {
4551
4793
  await this.mainGit.deleteBranch(record.branchName);
4552
4794
  } catch {
@@ -4556,8 +4798,40 @@ var PipelineOrchestrator = class {
4556
4798
  } catch {
4557
4799
  }
4558
4800
  });
4801
+ this.tracker.clearProcessingLock(issueIid);
4559
4802
  this.tracker.updateState(issueIid, "skipped" /* Skipped */);
4560
- logger20.info("Issue cancelled", { issueIid });
4803
+ await this.cleanupE2eOutputs(issueIid);
4804
+ logger18.info("Issue cancelled", { issueIid });
4805
+ }
4806
+ /**
4807
+ * Remove the E2E output directory for an issue: {uatVendorDir}/outputs/issue-{iid}
4808
+ */
4809
+ async cleanupE2eOutputs(issueIid) {
4810
+ const vendorDir = this.config.e2e.uatVendorDir;
4811
+ if (!vendorDir) return;
4812
+ const abs = path11.isAbsolute(vendorDir) ? vendorDir : path11.resolve(this.config.project.workDir, vendorDir);
4813
+ const outputDir = path11.join(abs, "outputs", `issue-${issueIid}`);
4814
+ try {
4815
+ await fs9.rm(outputDir, { recursive: true, force: true });
4816
+ logger18.info("E2E outputs cleaned up", { issueIid, dir: outputDir });
4817
+ } catch (err) {
4818
+ logger18.warn("Failed to cleanup E2E outputs", { issueIid, dir: outputDir, error: err.message });
4819
+ }
4820
+ }
4821
+ /**
4822
+ * When WorkspaceManager is active, the workspace root (which contains
4823
+ * associate repo clone dirs) may survive after cleanupWorktree removes
4824
+ * only the primary worktree. Force-remove the whole workspace root.
4825
+ */
4826
+ async cleanupWorkspaceRoot(issueIid) {
4827
+ if (!this.workspaceManager) return;
4828
+ const wsRoot = this.workspaceManager.getWorkspaceRoot(issueIid);
4829
+ try {
4830
+ await fs9.rm(wsRoot, { recursive: true, force: true });
4831
+ logger18.info("Workspace root cleaned up", { issueIid, dir: wsRoot });
4832
+ } catch (err) {
4833
+ logger18.warn("Failed to cleanup workspace root", { issueIid, dir: wsRoot, error: err.message });
4834
+ }
4561
4835
  }
4562
4836
  retryFromPhase(issueIid, phase) {
4563
4837
  const record = this.tracker.get(issueIid);
@@ -4567,7 +4841,12 @@ var PipelineOrchestrator = class {
4567
4841
  if (!issueLM.isRetryable(phase)) {
4568
4842
  throw new InvalidPhaseError(phase);
4569
4843
  }
4570
- logger20.info("Retrying issue from phase", { issueIid, phase });
4844
+ const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
4845
+ if (!this.aiRunner.interruptByWorkDir?.(wtCtx.workDir)) {
4846
+ this.aiRunner.killByWorkDir(wtCtx.workDir);
4847
+ }
4848
+ this.e2eAiRunner?.killByWorkDir(wtCtx.workDir);
4849
+ logger18.info("Retrying issue from phase", { issueIid, phase });
4571
4850
  const ok = this.tracker.resetToPhase(issueIid, phase, issueDef);
4572
4851
  if (!ok) {
4573
4852
  throw new InvalidPhaseError(phase);
@@ -4594,7 +4873,7 @@ var PipelineOrchestrator = class {
4594
4873
  } else {
4595
4874
  this.tracker.pauseIssue(issueIid, record.currentPhase ?? "");
4596
4875
  }
4597
- logger20.info("Issue abort requested", { issueIid, state: record.state });
4876
+ logger18.info("Issue abort requested", { issueIid, state: record.state });
4598
4877
  }
4599
4878
  continueIssue(issueIid) {
4600
4879
  const record = this.tracker.get(issueIid);
@@ -4604,7 +4883,7 @@ var PipelineOrchestrator = class {
4604
4883
  }
4605
4884
  const issueDef = this.getIssueSpecificPipelineDef(record);
4606
4885
  this.tracker.resumeFromPause(issueIid, issueDef, false);
4607
- logger20.info("Issue continued from pause", { issueIid });
4886
+ logger18.info("Issue continued from pause", { issueIid });
4608
4887
  }
4609
4888
  redoPhase(issueIid) {
4610
4889
  const record = this.tracker.get(issueIid);
@@ -4630,6 +4909,12 @@ var PipelineOrchestrator = class {
4630
4909
  if (phase) {
4631
4910
  const wtPlan = new PlanPersistence(wtCtx.workDir, issueIid);
4632
4911
  wtPlan.updatePhaseProgress(phase, "pending");
4912
+ this.tracker.updatePhaseProgress(issueIid, phase, {
4913
+ status: "pending",
4914
+ startedAt: void 0,
4915
+ completedAt: void 0,
4916
+ error: void 0
4917
+ });
4633
4918
  }
4634
4919
  this.tracker.resumeFromPause(issueIid, issueDef, true);
4635
4920
  this.eventBus.emitTyped("issue:redone", { issueIid });
@@ -4642,7 +4927,7 @@ var PipelineOrchestrator = class {
4642
4927
  }
4643
4928
  this.eventBus.emitTyped("issue:redone", { issueIid });
4644
4929
  }
4645
- logger20.info("Issue redo requested", { issueIid, state: record.state });
4930
+ logger18.info("Issue redo requested", { issueIid, state: record.state });
4646
4931
  }
4647
4932
  /**
4648
4933
  * 处理中止/重做的共享逻辑:
@@ -4697,7 +4982,6 @@ var PipelineOrchestrator = class {
4697
4982
  emitProgress: (iid, step, msg) => this.emitProgress(iid, step, msg),
4698
4983
  ensureWorktree: (wtCtx) => this.ensureWorktree(wtCtx),
4699
4984
  installDependencies: (workDir) => this.installDependencies(workDir),
4700
- buildPhaseHooks: (iid, issueId) => this.buildPhaseHooks(iid, issueId),
4701
4985
  shouldAutoApprove: (labels) => this.shouldAutoApprove(labels),
4702
4986
  shouldDeployServers: (iid) => this.shouldDeployServers(iid),
4703
4987
  startPreviewServers: (wtCtx, issue) => this.startPreviewServers(wtCtx, issue),
@@ -4716,7 +5000,7 @@ var PipelineOrchestrator = class {
4716
5000
  async _processIssueImpl(issue) {
4717
5001
  const branchName = `${this.config.project.branchPrefix}-${issue.iid}`;
4718
5002
  const wtCtx = this.computeWorktreeContext(issue.iid, branchName);
4719
- logger20.info("Processing issue", {
5003
+ logger18.info("Processing issue", {
4720
5004
  iid: issue.iid,
4721
5005
  title: issue.title,
4722
5006
  branchName,
@@ -4784,12 +5068,14 @@ var PipelineOrchestrator = class {
4784
5068
  await executeCompletion(ctx, deps, phaseResult, wtGitMap);
4785
5069
  } catch (err) {
4786
5070
  if (err instanceof PhaseAbortedError) {
5071
+ if (err.action === "restart") return;
4787
5072
  this.applyPendingAction(err.action, issue.iid, wtCtx, issuePipelineDef);
4788
5073
  return;
4789
5074
  }
4790
5075
  const pendingAction = this.pendingActions.get(issue.iid);
4791
5076
  if (pendingAction) {
4792
5077
  this.pendingActions.delete(issue.iid);
5078
+ if (pendingAction === "restart") return;
4793
5079
  this.applyPendingAction(pendingAction, issue.iid, wtCtx, issuePipelineDef);
4794
5080
  return;
4795
5081
  }
@@ -4821,7 +5107,7 @@ var PipelineOrchestrator = class {
4821
5107
  title,
4822
5108
  description
4823
5109
  });
4824
- logger20.info("Merge request created successfully", {
5110
+ logger18.info("Merge request created successfully", {
4825
5111
  iid: issue.iid,
4826
5112
  mrIid: mr.iid,
4827
5113
  mrUrl: mr.web_url
@@ -4829,7 +5115,7 @@ var PipelineOrchestrator = class {
4829
5115
  return { url: mr.web_url, iid: mr.iid };
4830
5116
  } catch (err) {
4831
5117
  const errorMsg = err.message;
4832
- logger20.warn("Failed to create merge request, trying to find existing one", {
5118
+ logger18.warn("Failed to create merge request, trying to find existing one", {
4833
5119
  iid: issue.iid,
4834
5120
  error: errorMsg
4835
5121
  });
@@ -4846,7 +5132,7 @@ var PipelineOrchestrator = class {
4846
5132
  this.config.project.baseBranch
4847
5133
  );
4848
5134
  if (existing) {
4849
- logger20.info("Found existing merge request", {
5135
+ logger18.info("Found existing merge request", {
4850
5136
  iid: issueIid,
4851
5137
  mrIid: existing.iid,
4852
5138
  mrUrl: existing.web_url
@@ -4854,7 +5140,7 @@ var PipelineOrchestrator = class {
4854
5140
  return { url: existing.web_url, iid: existing.iid };
4855
5141
  }
4856
5142
  } catch (findErr) {
4857
- logger20.warn("Failed to find existing merge request", {
5143
+ logger18.warn("Failed to find existing merge request", {
4858
5144
  iid: issueIid,
4859
5145
  error: findErr.message
4860
5146
  });
@@ -4871,32 +5157,6 @@ var PipelineOrchestrator = class {
4871
5157
  if (!autoLabels.length) return false;
4872
5158
  return issueLabels.some((l) => autoLabels.includes(l));
4873
5159
  }
4874
- buildPhaseHooks(iid, issueId) {
4875
- return {
4876
- onPhaseStart: async (phase) => {
4877
- this.tracker.updateState(iid, "phase_running" /* PhaseRunning */, { currentPhase: phase });
4878
- },
4879
- onPhaseDone: async (phase) => {
4880
- this.tracker.updateState(iid, "phase_done" /* PhaseDone */, { currentPhase: phase });
4881
- },
4882
- onPhaseFailed: async (_phase, error) => {
4883
- this.tracker.markFailed(iid, error, "phase_running" /* PhaseRunning */);
4884
- },
4885
- onComment: async (msg) => {
4886
- try {
4887
- await this.gongfeng.createIssueNote(issueId, msg);
4888
- } catch {
4889
- }
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
- },
4896
- isNoteSyncEnabled: () => isNoteSyncEnabledForIssue(iid, this.tracker, this.config),
4897
- isE2eEnabled: () => isE2eEnabledForIssue(iid, this.tracker, this.config)
4898
- };
4899
- }
4900
5160
  async startPreviewServers(wtCtx, issue) {
4901
5161
  try {
4902
5162
  this.emitProgress(issue.iid, "deploy", t("orchestrator.deployProgress"));
@@ -4925,7 +5185,7 @@ var PipelineOrchestrator = class {
4925
5185
  });
4926
5186
  return ports;
4927
5187
  } catch (err) {
4928
- logger20.error("Failed to start preview servers", {
5188
+ logger18.error("Failed to start preview servers", {
4929
5189
  iid: issue.iid,
4930
5190
  error: err.message
4931
5191
  });
@@ -4960,7 +5220,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4960
5220
  await this.mainGitMutex.runExclusive(async () => {
4961
5221
  await this.cleanupWorktree(wtCtx);
4962
5222
  });
4963
- logger20.info("Preview stopped and worktree cleaned", { iid: issueIid });
5223
+ logger18.info("Preview stopped and worktree cleaned", { iid: issueIid });
4964
5224
  }
4965
5225
  async markDeployed(issueIid) {
4966
5226
  const record = this.tracker.get(issueIid);
@@ -4977,7 +5237,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4977
5237
  try {
4978
5238
  await this.gongfeng.closeIssue(externalId);
4979
5239
  } catch (err) {
4980
- logger20.warn("Failed to close issue on Gongfeng", { iid: issueIid, error: err.message });
5240
+ logger18.warn("Failed to close issue on Gongfeng", { iid: issueIid, error: err.message });
4981
5241
  }
4982
5242
  try {
4983
5243
  const issue = await this.gongfeng.getIssueDetail(externalId);
@@ -4985,10 +5245,10 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
4985
5245
  labels.push("auto-finish:deployed");
4986
5246
  await this.gongfeng.updateIssueLabels(externalId, labels);
4987
5247
  } catch (err) {
4988
- logger20.warn("Failed to update labels", { iid: issueIid, error: err.message });
5248
+ logger18.warn("Failed to update labels", { iid: issueIid, error: err.message });
4989
5249
  }
4990
5250
  this.tracker.updateState(issueIid, "deployed" /* Deployed */);
4991
- logger20.info("Issue marked as deployed", { iid: issueIid });
5251
+ logger18.info("Issue marked as deployed", { iid: issueIid });
4992
5252
  }
4993
5253
  async restartPreview(issueIid) {
4994
5254
  const record = this.tracker.get(issueIid);
@@ -5015,7 +5275,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
5015
5275
  throw err;
5016
5276
  }
5017
5277
  const url = this.buildPreviewUrl(issueIid);
5018
- logger20.info("Preview restarted", { iid: issueIid, url });
5278
+ logger18.info("Preview restarted", { iid: issueIid, url });
5019
5279
  return url;
5020
5280
  }
5021
5281
  getPreviewHost() {
@@ -5048,7 +5308,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
5048
5308
  if (!record) throw new IssueNotFoundError(issueIid);
5049
5309
  const baseBranch = this.config.project.baseBranch;
5050
5310
  const branchName = record.branchName;
5051
- logger20.info("Starting conflict resolution", { issueIid, branchName, baseBranch });
5311
+ logger18.info("Starting conflict resolution", { issueIid, branchName, baseBranch });
5052
5312
  this.tracker.updateState(issueIid, "resolving_conflict" /* ResolvingConflict */);
5053
5313
  this.eventBus.emitTyped("conflict:started", { issueIid });
5054
5314
  try {
@@ -5081,12 +5341,10 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
5081
5341
  });
5082
5342
  }
5083
5343
  });
5084
- logger20.info("Running verification after conflict resolution", { issueIid });
5344
+ logger18.info("Running verification after conflict resolution", { issueIid });
5085
5345
  const wtPlan = new PlanPersistence(wtCtx.workDir, issueIid);
5086
5346
  wtPlan.ensureDir();
5087
- const conflictLM = createLifecycleManager(this.getIssueSpecificPipelineDef(record));
5088
- const conflictHooks = this.buildPhaseHooks(issueIid, getExternalId(record));
5089
- const verifyPhase = createPhase("verify", this.aiRunner, wtGit, wtPlan, this.config, conflictLM, conflictHooks);
5347
+ const verifyPhase = createPhase("verify", this.aiRunner, wtGit, wtPlan, this.config);
5090
5348
  const verifyCtx = {
5091
5349
  demand: {
5092
5350
  demandId: `gf-${issueIid}`,
@@ -5102,7 +5360,14 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
5102
5360
  branchName,
5103
5361
  pipelineMode: record.pipelineMode
5104
5362
  };
5105
- await verifyPhase.execute(verifyCtx);
5363
+ const verifyOutcome = await verifyPhase.run(verifyCtx);
5364
+ if (verifyOutcome.status === "failed") {
5365
+ const errMsg = verifyOutcome.error?.message ?? "Verification failed after conflict resolution";
5366
+ throw new (await import("./errors-S3BWYA4I.js")).AIExecutionError("verify", errMsg, {
5367
+ output: verifyOutcome.error?.rawOutput ?? verifyOutcome.output,
5368
+ exitCode: verifyOutcome.exitCode ?? 1
5369
+ });
5370
+ }
5106
5371
  await wtGit.forcePush(branchName);
5107
5372
  this.tracker.updateState(issueIid, "completed" /* Completed */);
5108
5373
  this.eventBus.emitTyped("conflict:resolved", { issueIid });
@@ -5114,10 +5379,10 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
5114
5379
  } catch {
5115
5380
  }
5116
5381
  await this.commentOnMr(record.mrUrl, t("conflict.mrResolvedComment"));
5117
- logger20.info("Conflict resolution completed", { issueIid });
5382
+ logger18.info("Conflict resolution completed", { issueIid });
5118
5383
  } catch (err) {
5119
5384
  const errorMsg = err.message;
5120
- logger20.error("Conflict resolution failed", { issueIid, error: errorMsg });
5385
+ logger18.error("Conflict resolution failed", { issueIid, error: errorMsg });
5121
5386
  try {
5122
5387
  const wtGit = new GitOperations(wtCtx.gitRootDir);
5123
5388
  if (await wtGit.isRebaseInProgress()) {
@@ -5147,7 +5412,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
5147
5412
  try {
5148
5413
  await this.gongfeng.createMergeRequestNote(mrIid, body);
5149
5414
  } catch (err) {
5150
- logger20.warn("Failed to comment on MR", { mrIid, error: err.message });
5415
+ logger18.warn("Failed to comment on MR", { mrIid, error: err.message });
5151
5416
  }
5152
5417
  }
5153
5418
  };
@@ -5223,7 +5488,7 @@ ${questions}
5223
5488
  }
5224
5489
 
5225
5490
  // src/services/BrainstormService.ts
5226
- var logger21 = logger.child("Brainstorm");
5491
+ var logger19 = logger.child("Brainstorm");
5227
5492
  function agentConfigToAIConfig(agentCfg, timeoutMs) {
5228
5493
  return {
5229
5494
  mode: agentCfg.mode,
@@ -5259,7 +5524,7 @@ var BrainstormService = class {
5259
5524
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
5260
5525
  };
5261
5526
  this.sessions.set(session.id, session);
5262
- logger21.info("Created brainstorm session", { sessionId: session.id });
5527
+ logger19.info("Created brainstorm session", { sessionId: session.id });
5263
5528
  return session;
5264
5529
  }
5265
5530
  getSession(id) {
@@ -5268,7 +5533,7 @@ var BrainstormService = class {
5268
5533
  async generate(sessionId, onEvent) {
5269
5534
  const session = this.requireSession(sessionId);
5270
5535
  session.status = "generating";
5271
- logger21.info("Generating SDD", { sessionId });
5536
+ logger19.info("Generating SDD", { sessionId });
5272
5537
  const prompt = buildGeneratePrompt(session.transcript);
5273
5538
  const result = await this.generatorRunner.run({
5274
5539
  prompt,
@@ -5294,7 +5559,7 @@ var BrainstormService = class {
5294
5559
  const session = this.requireSession(sessionId);
5295
5560
  const roundNum = session.rounds.length + 1;
5296
5561
  session.status = "reviewing";
5297
- logger21.info("Reviewing SDD", { sessionId, round: roundNum });
5562
+ logger19.info("Reviewing SDD", { sessionId, round: roundNum });
5298
5563
  onEvent?.({ type: "round:start", data: { round: roundNum, phase: "review" }, round: roundNum });
5299
5564
  const prompt = buildReviewPrompt(session.currentSdd, roundNum);
5300
5565
  const result = await this.reviewerRunner.run({
@@ -5327,7 +5592,7 @@ var BrainstormService = class {
5327
5592
  throw new Error("No review round to refine from");
5328
5593
  }
5329
5594
  session.status = "refining";
5330
- logger21.info("Refining SDD", { sessionId, round: currentRound.round });
5595
+ logger19.info("Refining SDD", { sessionId, round: currentRound.round });
5331
5596
  const prompt = buildRefinePrompt(currentRound.questions);
5332
5597
  const result = await this.generatorRunner.run({
5333
5598
  prompt,
@@ -5388,9 +5653,6 @@ export {
5388
5653
  IssueState,
5389
5654
  IssueTracker,
5390
5655
  PlanPersistence,
5391
- getNoteSyncEnabled,
5392
- setNoteSyncOverride,
5393
- isNoteSyncEnabledForIssue,
5394
5656
  BasePhase,
5395
5657
  registerPhase,
5396
5658
  validatePhaseRegistry,
@@ -5409,7 +5671,10 @@ export {
5409
5671
  buildSingleRepoWorkspace,
5410
5672
  isMultiRepo,
5411
5673
  persistWorkspaceConfig,
5674
+ getNoteSyncEnabled,
5675
+ setNoteSyncOverride,
5676
+ isNoteSyncEnabledForIssue,
5412
5677
  PipelineOrchestrator,
5413
5678
  BrainstormService
5414
5679
  };
5415
- //# sourceMappingURL=chunk-5UPYA6KH.js.map
5680
+ //# sourceMappingURL=chunk-IP3QTP5A.js.map