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