@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.
- package/dist/AIRunnerRegistry-II3WWSFN.js +31 -0
- package/dist/PtyRunner-6UGI5STW.js +22 -0
- package/dist/TerminalManager-RT2N7N5R.js +8 -0
- package/dist/ai-runner/AIRunner.d.ts +9 -1
- package/dist/ai-runner/AIRunner.d.ts.map +1 -1
- package/dist/ai-runner/AIRunnerRegistry.d.ts +37 -1
- package/dist/ai-runner/AIRunnerRegistry.d.ts.map +1 -1
- package/dist/ai-runner/PtyRunner.d.ts +114 -0
- package/dist/ai-runner/PtyRunner.d.ts.map +1 -0
- package/dist/ai-runner/index.d.ts +3 -1
- package/dist/ai-runner/index.d.ts.map +1 -1
- package/dist/{ai-runner-SVUNA3FX.js → ai-runner-HLA44WI6.js} +12 -3
- package/dist/{analyze-SXXPE5XL.js → analyze-ZIXNC5GN.js} +10 -8
- package/dist/{analyze-SXXPE5XL.js.map → analyze-ZIXNC5GN.js.map} +1 -1
- package/dist/{braindump-4E5SDMSZ.js → braindump-56WAY2RD.js} +10 -8
- package/dist/{braindump-4E5SDMSZ.js.map → braindump-56WAY2RD.js.map} +1 -1
- package/dist/{chunk-JINMYD56.js → chunk-2MESXJEZ.js} +3 -3
- package/dist/{chunk-P4O4ZXEC.js → chunk-2YQHKXLL.js} +40 -19
- package/dist/chunk-2YQHKXLL.js.map +1 -0
- package/dist/chunk-AVGZH64A.js +211 -0
- package/dist/chunk-AVGZH64A.js.map +1 -0
- package/dist/{chunk-5UPYA6KH.js → chunk-IP3QTP5A.js} +1028 -763
- package/dist/chunk-IP3QTP5A.js.map +1 -0
- package/dist/chunk-KC5S66OZ.js +177 -0
- package/dist/chunk-KC5S66OZ.js.map +1 -0
- package/dist/{chunk-4QV6D34Y.js → chunk-M5C2WILQ.js} +8 -6
- package/dist/{chunk-4QV6D34Y.js.map → chunk-M5C2WILQ.js.map} +1 -1
- package/dist/{chunk-FWEW5E3B.js → chunk-NZHKAPU6.js} +35 -5
- package/dist/chunk-NZHKAPU6.js.map +1 -0
- package/dist/{chunk-KTYPZTF4.js → chunk-O3WEV5W3.js} +10 -2
- package/dist/chunk-O3WEV5W3.js.map +1 -0
- package/dist/{chunk-K2OTLYJI.js → chunk-QZZGIZWC.js} +457 -202
- package/dist/chunk-QZZGIZWC.js.map +1 -0
- package/dist/{chunk-4LFNFRCL.js → chunk-SAMTXC4A.js} +91 -214
- package/dist/chunk-SAMTXC4A.js.map +1 -0
- package/dist/chunk-U237JSLB.js +1 -0
- package/dist/chunk-U237JSLB.js.map +1 -0
- package/dist/chunk-U6GWFTKA.js +657 -0
- package/dist/chunk-U6GWFTKA.js.map +1 -0
- package/dist/{chunk-HOFYJEJ4.js → chunk-UBQLXQ7I.js} +11 -11
- package/dist/cli/setup/env-metadata.d.ts.map +1 -1
- package/dist/cli.js +8 -7
- package/dist/cli.js.map +1 -1
- package/dist/{config-QLINHCHD.js → config-WTRSZLOC.js} +4 -3
- package/dist/config-WTRSZLOC.js.map +1 -0
- package/dist/config-schema.d.ts +17 -1
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config.d.ts +20 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/errors/PhaseAbortedError.d.ts +3 -3
- package/dist/errors/PhaseAbortedError.d.ts.map +1 -1
- package/dist/errors-S3BWYA4I.js +43 -0
- package/dist/errors-S3BWYA4I.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -11
- package/dist/{init-TDKDC6YP.js → init-QQDXGTPB.js} +7 -6
- package/dist/{init-TDKDC6YP.js.map → init-QQDXGTPB.js.map} +1 -1
- package/dist/lib.js +9 -7
- package/dist/lib.js.map +1 -1
- package/dist/orchestrator/IssueProcessingContext.d.ts +39 -21
- package/dist/orchestrator/IssueProcessingContext.d.ts.map +1 -1
- package/dist/orchestrator/PipelineOrchestrator.d.ts +10 -1
- package/dist/orchestrator/PipelineOrchestrator.d.ts.map +1 -1
- package/dist/orchestrator/steps/PhaseLoopStep.d.ts +1 -1
- package/dist/orchestrator/steps/PhaseLoopStep.d.ts.map +1 -1
- package/dist/orchestrator/steps/SetupStep.d.ts.map +1 -1
- package/dist/persistence/PlanPersistence.d.ts +7 -1
- package/dist/persistence/PlanPersistence.d.ts.map +1 -1
- package/dist/phases/BasePhase.d.ts +31 -42
- package/dist/phases/BasePhase.d.ts.map +1 -1
- package/dist/phases/BuildPhase.d.ts.map +1 -1
- package/dist/phases/PhaseFactory.d.ts +2 -3
- package/dist/phases/PhaseFactory.d.ts.map +1 -1
- package/dist/phases/PhaseOutcome.d.ts +42 -0
- package/dist/phases/PhaseOutcome.d.ts.map +1 -0
- package/dist/phases/PlanPhase.d.ts +1 -1
- package/dist/phases/PlanPhase.d.ts.map +1 -1
- package/dist/phases/ReleasePhase.d.ts +8 -18
- package/dist/phases/ReleasePhase.d.ts.map +1 -1
- package/dist/phases/UatPhase.d.ts +7 -24
- package/dist/phases/UatPhase.d.ts.map +1 -1
- package/dist/phases/VerifyPhase.d.ts +4 -4
- package/dist/phases/VerifyPhase.d.ts.map +1 -1
- package/dist/poller/IssuePoller.d.ts.map +1 -1
- package/dist/prompts/release-templates.d.ts.map +1 -1
- package/dist/prompts/templates.d.ts.map +1 -1
- package/dist/{restart-RNXGTDWZ.js → restart-BMILTP5X.js} +6 -5
- package/dist/{restart-RNXGTDWZ.js.map → restart-BMILTP5X.js.map} +1 -1
- package/dist/run.js +14 -11
- package/dist/run.js.map +1 -1
- package/dist/settings/ExperimentalSettings.d.ts +1 -1
- package/dist/settings/ExperimentalSettings.d.ts.map +1 -1
- package/dist/start-6QRW6IJI.js +15 -0
- package/dist/start-6QRW6IJI.js.map +1 -0
- package/dist/terminal/TerminalManager.d.ts +62 -0
- package/dist/terminal/TerminalManager.d.ts.map +1 -0
- package/dist/terminal/TerminalWebSocket.d.ts +9 -0
- package/dist/terminal/TerminalWebSocket.d.ts.map +1 -0
- package/dist/tracker/ExecutableTask.d.ts +4 -2
- package/dist/tracker/ExecutableTask.d.ts.map +1 -1
- package/dist/tracker/IssueState.d.ts +11 -1
- package/dist/tracker/IssueState.d.ts.map +1 -1
- package/dist/tracker/IssueTracker.d.ts +19 -1
- package/dist/tracker/IssueTracker.d.ts.map +1 -1
- package/dist/web/WebServer.d.ts +4 -0
- package/dist/web/WebServer.d.ts.map +1 -1
- package/dist/web/routes/terminal.d.ts +11 -0
- package/dist/web/routes/terminal.d.ts.map +1 -0
- package/dist/webhook/CommandExecutor.d.ts.map +1 -1
- package/package.json +7 -1
- package/src/web/frontend/dist/assets/index-COYziOhv.css +1 -0
- package/src/web/frontend/dist/assets/index-D_oTMuJU.js +151 -0
- package/src/web/frontend/dist/index.html +2 -2
- package/dist/chunk-4LFNFRCL.js.map +0 -1
- package/dist/chunk-5UPYA6KH.js.map +0 -1
- package/dist/chunk-DADQSKPL.js +0 -1
- package/dist/chunk-FWEW5E3B.js.map +0 -1
- package/dist/chunk-K2OTLYJI.js.map +0 -1
- package/dist/chunk-KTYPZTF4.js.map +0 -1
- package/dist/chunk-P4O4ZXEC.js.map +0 -1
- package/dist/start-27GRO4DP.js +0 -14
- package/src/web/frontend/dist/assets/index-C4NXoH9S.js +0 -133
- package/src/web/frontend/dist/assets/index-C7lorIa0.css +0 -1
- /package/dist/{ai-runner-SVUNA3FX.js.map → AIRunnerRegistry-II3WWSFN.js.map} +0 -0
- /package/dist/{chunk-DADQSKPL.js.map → PtyRunner-6UGI5STW.js.map} +0 -0
- /package/dist/{config-QLINHCHD.js.map → TerminalManager-RT2N7N5R.js.map} +0 -0
- /package/dist/{start-27GRO4DP.js.map → ai-runner-HLA44WI6.js.map} +0 -0
- /package/dist/{chunk-JINMYD56.js.map → chunk-2MESXJEZ.js.map} +0 -0
- /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-
|
|
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
|
-
|
|
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(
|
|
232
|
-
const url = `${this.projectApiBase}${
|
|
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"} ${
|
|
251
|
+
}, `requestRaw ${options.method || "GET"} ${path12}`)
|
|
250
252
|
);
|
|
251
253
|
}
|
|
252
|
-
async request(
|
|
253
|
-
const resp = await this.requestRaw(
|
|
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(
|
|
433
|
-
const url = `${this.apiUrl}${
|
|
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"} ${
|
|
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
|
-
|
|
1125
|
-
if (
|
|
1126
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1630
|
-
|
|
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
|
-
|
|
1653
|
-
const fullPrompt = matchedRulesText ? `${basePrompt}
|
|
1738
|
+
const rules = await this.resolveRules(ctx);
|
|
1739
|
+
if (rules) prompt += `
|
|
1654
1740
|
|
|
1655
|
-
${t("basePhase.rulesSection", { rules
|
|
1656
|
-
const
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1679
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
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
|
|
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
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
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
|
|
2087
|
-
const
|
|
2088
|
-
if (
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
2146
|
-
|
|
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 =
|
|
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
|
|
2240
|
-
import
|
|
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 =
|
|
2204
|
+
this.cacheDir = path5.join(dataDir, "release-detect");
|
|
2250
2205
|
}
|
|
2251
2206
|
filePath(projectPath) {
|
|
2252
|
-
return
|
|
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 (!
|
|
2261
|
-
const raw =
|
|
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 (!
|
|
2285
|
-
|
|
2239
|
+
if (!fs3.existsSync(this.cacheDir)) {
|
|
2240
|
+
fs3.mkdirSync(this.cacheDir, { recursive: true });
|
|
2286
2241
|
}
|
|
2287
|
-
|
|
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 (
|
|
2300
|
-
|
|
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\
|
|
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
|
-
## \
|
|
2272
|
+
## \u5173\u952E\u5224\u5B9A\u539F\u5219
|
|
2318
2273
|
|
|
2319
|
-
\
|
|
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
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
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
|
-
|
|
2326
|
-
- \`.cursor/rules/\` \u76EE\u5F55\u4E0B\u4E0E release/deploy \u76F8\u5173\u7684 \`.mdc\` \u89C4\u5219
|
|
2285
|
+
## \u63A2\u7D22\u8303\u56F4
|
|
2327
2286
|
|
|
2328
|
-
|
|
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
|
-
|
|
2333
|
-
-
|
|
2334
|
-
-
|
|
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
|
-
|
|
2298
|
+
3. **\u53D1\u5E03\u811A\u672C**
|
|
2337
2299
|
- \`scripts/release.*\`\u3001\`scripts/deploy.*\`\u3001\`scripts/publish.*\`
|
|
2338
|
-
- \`
|
|
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}/
|
|
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":
|
|
2348
|
-
"releaseMethod": "
|
|
2349
|
-
"artifacts": ["
|
|
2350
|
-
"instructions": "\
|
|
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\
|
|
2356
|
-
- \`releaseMethod\`: \u53D1\u5E03\u65B9\u5F0F\
|
|
2357
|
-
- \`artifacts\`: \u53D1\u73B0\u7684\u53D1\u5E03\u76F8\u5173\u6587\u4EF6\u8DEF\u5F84\u6570\u7EC4
|
|
2358
|
-
- \`instructions\`: \
|
|
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
|
-
- \
|
|
2362
|
-
- \u5982\u679C\u627E\u5230\u4E86 skill \u6216 rule \u6587\u4EF6\uFF0C\u8BF7\u5728 \`instructions\` \u4E2D\u5B8C\u6574\u4FDD\u7559\u539F\u6587\u5185\u5BB9
|
|
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}/
|
|
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
|
|
2384
|
+
return this.plan.readFile(DETECT_FILENAME) !== null;
|
|
2425
2385
|
}
|
|
2426
2386
|
readDetectionFile() {
|
|
2427
|
-
const
|
|
2428
|
-
if (
|
|
2387
|
+
const raw = this.plan.readFile(DETECT_FILENAME);
|
|
2388
|
+
if (raw === null) return null;
|
|
2429
2389
|
try {
|
|
2430
|
-
return JSON.parse(
|
|
2390
|
+
return JSON.parse(raw);
|
|
2431
2391
|
} catch {
|
|
2432
2392
|
return null;
|
|
2433
2393
|
}
|
|
2434
2394
|
}
|
|
2435
2395
|
writeDetectionFile(result) {
|
|
2436
|
-
|
|
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
|
|
2464
|
-
|
|
2465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2434
|
+
status: "completed",
|
|
2488
2435
|
output: `Release detection cached: ${cached.hasReleaseCapability ? cached.releaseMethod ?? "found" : "no capability"}`,
|
|
2489
|
-
|
|
2490
|
-
gateRequested: cached.hasReleaseCapability,
|
|
2491
|
-
hasReleaseCapability: cached.hasReleaseCapability
|
|
2436
|
+
data: { hasReleaseCapability: cached.hasReleaseCapability }
|
|
2492
2437
|
};
|
|
2493
2438
|
}
|
|
2494
2439
|
}
|
|
2495
|
-
const
|
|
2496
|
-
if (
|
|
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
|
-
|
|
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
|
-
...
|
|
2513
|
-
|
|
2514
|
-
hasReleaseCapability: detected.hasReleaseCapability
|
|
2458
|
+
...outcome,
|
|
2459
|
+
data: { ...outcome.data, hasReleaseCapability: detected.hasReleaseCapability }
|
|
2515
2460
|
};
|
|
2516
2461
|
}
|
|
2517
|
-
return { ...
|
|
2462
|
+
return { ...outcome, data: { ...outcome.data, hasReleaseCapability: false } };
|
|
2518
2463
|
}
|
|
2519
|
-
return
|
|
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(
|
|
2544
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
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
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
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
|
|
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
|
|
2790
|
-
import
|
|
2791
|
-
var
|
|
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 (!
|
|
2808
|
-
|
|
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 =
|
|
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
|
-
|
|
2742
|
+
logger8.error(`Workspace config validation failed:
|
|
2820
2743
|
${issues}`);
|
|
2821
2744
|
return null;
|
|
2822
2745
|
}
|
|
2823
|
-
|
|
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
|
-
|
|
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 =
|
|
2856
|
-
if (!
|
|
2857
|
-
|
|
2778
|
+
const dir = path6.dirname(filePath);
|
|
2779
|
+
if (!fs4.existsSync(dir)) {
|
|
2780
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
2858
2781
|
}
|
|
2859
|
-
|
|
2860
|
-
|
|
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
|
-
|
|
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
|
|
2871
|
-
import
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2885
|
+
logger9.info("Primary worktree removed", { dir: wsCtx.primary.gitRootDir });
|
|
2963
2886
|
} catch (err) {
|
|
2964
|
-
|
|
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
|
|
2972
|
-
|
|
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
|
-
|
|
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
|
|
2905
|
+
const entries = await fs5.readdir(wsCtx.workspaceRoot);
|
|
2983
2906
|
if (entries.length === 0) {
|
|
2984
|
-
await
|
|
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 =
|
|
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:
|
|
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:
|
|
3013
|
-
workDir:
|
|
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
|
|
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 =
|
|
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:
|
|
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 =
|
|
2965
|
+
const wsRoot = path7.dirname(repoDir);
|
|
3043
2966
|
if (wsRoot !== repoDir) {
|
|
3044
2967
|
try {
|
|
3045
|
-
await
|
|
3046
|
-
|
|
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
|
|
3057
|
-
|
|
2979
|
+
await fs5.access(path7.join(repoDir, ".git"));
|
|
2980
|
+
logger9.info("Reusing existing primary worktree", { dir: repoDir });
|
|
3058
2981
|
return;
|
|
3059
2982
|
} catch {
|
|
3060
|
-
|
|
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 =
|
|
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(
|
|
3005
|
+
const gitDirExists = await this.dirExists(path7.join(repoDir, ".git"));
|
|
3083
3006
|
if (!gitDirExists) {
|
|
3084
3007
|
await this.cleanStaleDir(repoDir);
|
|
3085
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
3123
|
-
await
|
|
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
|
|
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
|
|
3138
|
-
import
|
|
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
|
|
3172
|
-
import
|
|
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 =
|
|
3217
|
-
if (
|
|
3218
|
-
const content =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3208
|
+
logger10.info("Ports allocated", { issueIid, ...pair });
|
|
3286
3209
|
return pair;
|
|
3287
3210
|
}
|
|
3288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3321
|
-
import
|
|
3322
|
-
var
|
|
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 =
|
|
3331
|
-
if (!
|
|
3332
|
-
|
|
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 =
|
|
3337
|
-
return
|
|
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
|
-
|
|
3264
|
+
logger11.info("Servers already running for issue", { issueIid: wtCtx.issueIid });
|
|
3342
3265
|
return;
|
|
3343
3266
|
}
|
|
3344
|
-
|
|
3345
|
-
const backendLogPath =
|
|
3346
|
-
const frontendLogPath =
|
|
3347
|
-
const backendLog =
|
|
3348
|
-
const frontendLog =
|
|
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
|
-
|
|
3295
|
+
logger11.info("Backend process exited", { issueIid: wtCtx.issueIid, code });
|
|
3373
3296
|
});
|
|
3374
|
-
const frontendDir =
|
|
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
|
-
|
|
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
|
-
|
|
3331
|
+
logger11.info("Dev servers spawned, waiting for startup", { issueIid: wtCtx.issueIid, ...ports });
|
|
3409
3332
|
await new Promise((r) => setTimeout(r, 1e4));
|
|
3410
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3481
|
-
import
|
|
3482
|
-
var
|
|
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
|
|
3486
|
-
const full =
|
|
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 =
|
|
3497
|
-
if (!
|
|
3498
|
-
|
|
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
|
-
|
|
3426
|
+
logger12.debug("No screenshots found");
|
|
3504
3427
|
return [];
|
|
3505
3428
|
}
|
|
3506
3429
|
const screenshots = pngFiles.map((filePath) => {
|
|
3507
|
-
const relative =
|
|
3508
|
-
const testName = relative.split(
|
|
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
|
-
|
|
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
|
-
|
|
3441
|
+
logger12.info("Screenshots collected", { count: screenshots.length });
|
|
3519
3442
|
return screenshots;
|
|
3520
3443
|
}
|
|
3521
3444
|
|
|
3522
3445
|
// src/e2e/ScreenshotPublisher.ts
|
|
3523
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4001
|
+
logger15.info("Ports allocated but servers not running, restarting", { iid: issue.iid });
|
|
3876
4002
|
} else {
|
|
3877
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4113
|
+
logger15.info("UAT phase starting", { iid: issue.iid, runner: runnerName });
|
|
3939
4114
|
}
|
|
3940
|
-
const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config
|
|
3941
|
-
if (wtGitMap
|
|
4115
|
+
const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config);
|
|
4116
|
+
if (wtGitMap) {
|
|
3942
4117
|
phase.setWtGitMap(wtGitMap);
|
|
3943
4118
|
}
|
|
3944
|
-
const
|
|
3945
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4231
|
+
let verifyOutcome;
|
|
4006
4232
|
try {
|
|
4007
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
4276
|
+
logger15.info("Verify-fix loop passed", {
|
|
4042
4277
|
iteration,
|
|
4043
4278
|
iid: issue.iid
|
|
4044
4279
|
});
|
|
4045
4280
|
return;
|
|
4046
4281
|
}
|
|
4047
|
-
|
|
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
|
-
|
|
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,
|
|
4077
|
-
|
|
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
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4402
|
+
logger16.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
|
|
4167
4403
|
} catch (err) {
|
|
4168
|
-
|
|
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
|
-
|
|
4412
|
+
logger16.info("Issue processing completed", { iid: issue.iid });
|
|
4177
4413
|
}
|
|
4178
4414
|
|
|
4179
4415
|
// src/orchestrator/steps/FailureHandler.ts
|
|
4180
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 :
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
4566
|
+
const gitFile = path11.join(wtDir, ".git");
|
|
4331
4567
|
try {
|
|
4332
|
-
await
|
|
4568
|
+
await fs9.access(gitFile);
|
|
4333
4569
|
} catch {
|
|
4334
|
-
|
|
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
|
-
|
|
4579
|
+
logger18.warn("Aborting residual rebase in worktree", { dir: wtDir });
|
|
4344
4580
|
await wtGit.rebaseAbort();
|
|
4345
4581
|
cleaned++;
|
|
4346
4582
|
}
|
|
4347
|
-
const indexLock =
|
|
4583
|
+
const indexLock = path11.join(wtDir, ".git", "index.lock");
|
|
4348
4584
|
try {
|
|
4349
|
-
await
|
|
4350
|
-
|
|
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
|
-
|
|
4591
|
+
logger18.warn("Failed to clean worktree state", { dir: wtDir, error: err.message });
|
|
4356
4592
|
}
|
|
4357
4593
|
}
|
|
4358
4594
|
} catch (err) {
|
|
4359
|
-
|
|
4595
|
+
logger18.warn("Failed to list worktrees for cleanup", { error: err.message });
|
|
4360
4596
|
}
|
|
4361
|
-
const mainIndexLock =
|
|
4597
|
+
const mainIndexLock = path11.join(repoGitRoot, ".git", "index.lock");
|
|
4362
4598
|
try {
|
|
4363
|
-
await
|
|
4364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4662
|
+
logger18.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
|
|
4427
4663
|
} catch (err) {
|
|
4428
|
-
|
|
4664
|
+
logger18.warn("Failed to cleanup worktree", { dir: wtCtx.gitRootDir, error: err.message });
|
|
4429
4665
|
}
|
|
4430
4666
|
}
|
|
4431
4667
|
async installDependencies(workDir) {
|
|
4432
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4688
|
+
logger18.info("Dependencies installed");
|
|
4453
4689
|
} catch (err) {
|
|
4454
4690
|
if (fallbackCmd) {
|
|
4455
|
-
|
|
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
|
-
|
|
4701
|
+
logger18.info("Dependencies installed (fallback)");
|
|
4466
4702
|
} catch (retryErr) {
|
|
4467
|
-
|
|
4703
|
+
logger18.warn("Fallback install also failed", {
|
|
4468
4704
|
error: retryErr.message
|
|
4469
4705
|
});
|
|
4470
4706
|
}
|
|
4471
4707
|
} else {
|
|
4472
|
-
|
|
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 =
|
|
4715
|
+
const targetBin = path11.join(workDir, "node_modules", ".bin");
|
|
4480
4716
|
try {
|
|
4481
|
-
await
|
|
4482
|
-
|
|
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 =
|
|
4487
|
-
const targetNM =
|
|
4722
|
+
const sourceNM = path11.join(this.config.project.workDir, "node_modules");
|
|
4723
|
+
const targetNM = path11.join(workDir, "node_modules");
|
|
4488
4724
|
try {
|
|
4489
|
-
await
|
|
4725
|
+
await fs9.access(sourceNM);
|
|
4490
4726
|
} catch {
|
|
4491
|
-
|
|
4727
|
+
logger18.warn("Main repo node_modules not found, skipping seed", { sourceNM });
|
|
4492
4728
|
return false;
|
|
4493
4729
|
}
|
|
4494
|
-
|
|
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
|
-
|
|
4736
|
+
logger18.info("node_modules seeded from main repo");
|
|
4501
4737
|
return true;
|
|
4502
4738
|
} catch (err) {
|
|
4503
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4756
|
+
logger18.info("Agent notes cleaned up", { issueIid, deleted });
|
|
4519
4757
|
} catch (err) {
|
|
4520
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5248
|
+
logger18.warn("Failed to update labels", { iid: issueIid, error: err.message });
|
|
4989
5249
|
}
|
|
4990
5250
|
this.tracker.updateState(issueIid, "deployed" /* Deployed */);
|
|
4991
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5344
|
+
logger18.info("Running verification after conflict resolution", { issueIid });
|
|
5085
5345
|
const wtPlan = new PlanPersistence(wtCtx.workDir, issueIid);
|
|
5086
5346
|
wtPlan.ensureDir();
|
|
5087
|
-
const
|
|
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.
|
|
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
|
-
|
|
5382
|
+
logger18.info("Conflict resolution completed", { issueIid });
|
|
5118
5383
|
} catch (err) {
|
|
5119
5384
|
const errorMsg = err.message;
|
|
5120
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
5680
|
+
//# sourceMappingURL=chunk-IP3QTP5A.js.map
|