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