@xdevops/issue-auto-finish 1.0.91 → 1.0.93
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/{PtyRunner-NYASBTRP.js → PtyRunner-XMWDMH3L.js} +4 -2
- package/dist/ai-runner/DialogClassifier.d.ts +44 -0
- package/dist/ai-runner/DialogClassifier.d.ts.map +1 -0
- package/dist/ai-runner/PtyRunner.d.ts +17 -0
- package/dist/ai-runner/PtyRunner.d.ts.map +1 -1
- package/dist/{ai-runner-TOHVJJ76.js → ai-runner-S2ATTGWX.js} +2 -2
- package/dist/{analyze-DBH4K3J7.js → analyze-DAVYPBHK.js} +2 -2
- package/dist/{braindump-RYI4BGMG.js → braindump-A4R3A4QT.js} +2 -2
- package/dist/{chunk-ENF24C44.js → chunk-2XACBKPB.js} +2 -2
- package/dist/{chunk-UMQYEYLO.js → chunk-BPVRMZU4.js} +9 -9
- package/dist/{chunk-4XMYOXGZ.js → chunk-HD6V7KPE.js} +878 -67
- package/dist/chunk-HD6V7KPE.js.map +1 -0
- package/dist/{chunk-LDGK5NMS.js → chunk-OPWP73PW.js} +797 -1056
- package/dist/chunk-OPWP73PW.js.map +1 -0
- package/dist/{chunk-CQ66LL7P.js → chunk-SNSEW7DS.js} +1 -1
- package/dist/cli.js +5 -5
- package/dist/hooks/HookInjector.d.ts +14 -0
- package/dist/hooks/HookInjector.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/{init-UKTP7LXS.js → init-OD7CLRWK.js} +2 -2
- package/dist/lib.js +2 -2
- package/dist/lifecycle/DefaultLifecycleHook.d.ts +21 -0
- package/dist/lifecycle/DefaultLifecycleHook.d.ts.map +1 -0
- package/dist/lifecycle/FeedbackTypes.d.ts +52 -0
- package/dist/lifecycle/FeedbackTypes.d.ts.map +1 -0
- package/dist/lifecycle/PhaseLifecycleHook.d.ts +70 -0
- package/dist/lifecycle/PhaseLifecycleHook.d.ts.map +1 -0
- package/dist/lifecycle/PhaseMiddleware.d.ts +47 -0
- package/dist/lifecycle/PhaseMiddleware.d.ts.map +1 -0
- package/dist/lifecycle/PhaseStateMachine.d.ts +111 -0
- package/dist/lifecycle/PhaseStateMachine.d.ts.map +1 -0
- package/dist/lifecycle/index.d.ts +8 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/orchestrator/steps/PhaseHelpers.d.ts +24 -0
- package/dist/orchestrator/steps/PhaseHelpers.d.ts.map +1 -0
- package/dist/orchestrator/steps/PhaseLoopStep.d.ts +10 -0
- package/dist/orchestrator/steps/PhaseLoopStep.d.ts.map +1 -1
- package/dist/orchestrator/strategies/AiPhaseStrategy.d.ts +17 -0
- package/dist/orchestrator/strategies/AiPhaseStrategy.d.ts.map +1 -0
- package/dist/orchestrator/strategies/GateStrategy.d.ts +15 -0
- package/dist/orchestrator/strategies/GateStrategy.d.ts.map +1 -0
- package/dist/orchestrator/strategies/PhaseStrategy.d.ts +16 -0
- package/dist/orchestrator/strategies/PhaseStrategy.d.ts.map +1 -0
- package/dist/orchestrator/strategies/VerifyFixStrategy.d.ts +15 -0
- package/dist/orchestrator/strategies/VerifyFixStrategy.d.ts.map +1 -0
- package/dist/orchestrator/strategies/index.d.ts +17 -0
- package/dist/orchestrator/strategies/index.d.ts.map +1 -0
- package/dist/{restart-MSUWF4ID.js → restart-JVVOYC6C.js} +2 -2
- package/dist/run.js +4 -4
- package/dist/{start-B4CDZAFG.js → start-INU24RRG.js} +2 -2
- package/package.json +1 -1
- package/src/web/frontend/dist/assets/index-BrvoaFSK.css +1 -0
- package/src/web/frontend/dist/assets/{index-BoYtsxGN.js → index-CmyxgdS_.js} +54 -54
- package/src/web/frontend/dist/index.html +2 -2
- package/dist/chunk-4XMYOXGZ.js.map +0 -1
- package/dist/chunk-LDGK5NMS.js.map +0 -1
- package/src/web/frontend/dist/assets/index-DWOHf3bd.css +0 -1
- /package/dist/{PtyRunner-NYASBTRP.js.map → PtyRunner-XMWDMH3L.js.map} +0 -0
- /package/dist/{ai-runner-TOHVJJ76.js.map → ai-runner-S2ATTGWX.js.map} +0 -0
- /package/dist/{analyze-DBH4K3J7.js.map → analyze-DAVYPBHK.js.map} +0 -0
- /package/dist/{braindump-RYI4BGMG.js.map → braindump-A4R3A4QT.js.map} +0 -0
- /package/dist/{chunk-ENF24C44.js.map → chunk-2XACBKPB.js.map} +0 -0
- /package/dist/{chunk-UMQYEYLO.js.map → chunk-BPVRMZU4.js.map} +0 -0
- /package/dist/{chunk-CQ66LL7P.js.map → chunk-SNSEW7DS.js.map} +0 -0
- /package/dist/{init-UKTP7LXS.js.map → init-OD7CLRWK.js.map} +0 -0
- /package/dist/{restart-MSUWF4ID.js.map → restart-JVVOYC6C.js.map} +0 -0
- /package/dist/{start-B4CDZAFG.js.map → start-INU24RRG.js.map} +0 -0
|
@@ -18,6 +18,9 @@ import {
|
|
|
18
18
|
rePlanPrompt,
|
|
19
19
|
verifyPrompt
|
|
20
20
|
} from "./chunk-GPZX4DSY.js";
|
|
21
|
+
import {
|
|
22
|
+
HookInjector
|
|
23
|
+
} from "./chunk-HD6V7KPE.js";
|
|
21
24
|
import {
|
|
22
25
|
getProjectKnowledge
|
|
23
26
|
} from "./chunk-ACVOOHAR.js";
|
|
@@ -233,8 +236,8 @@ var GongfengClient = class {
|
|
|
233
236
|
const encoded = encodeURIComponent(this.projectPath);
|
|
234
237
|
return `${this.apiUrl}/api/v3/projects/${encoded}`;
|
|
235
238
|
}
|
|
236
|
-
async requestRaw(
|
|
237
|
-
const url = `${this.projectApiBase}${
|
|
239
|
+
async requestRaw(path12, options = {}) {
|
|
240
|
+
const url = `${this.projectApiBase}${path12}`;
|
|
238
241
|
logger4.debug("API request", { method: options.method || "GET", url });
|
|
239
242
|
return this.circuitBreaker.execute(
|
|
240
243
|
() => this.retryPolicy.execute(async () => {
|
|
@@ -251,11 +254,11 @@ var GongfengClient = class {
|
|
|
251
254
|
throw new GongfengApiError(resp.status, `Gongfeng API error ${resp.status}: ${body}`, body);
|
|
252
255
|
}
|
|
253
256
|
return resp;
|
|
254
|
-
}, `requestRaw ${options.method || "GET"} ${
|
|
257
|
+
}, `requestRaw ${options.method || "GET"} ${path12}`)
|
|
255
258
|
);
|
|
256
259
|
}
|
|
257
|
-
async request(
|
|
258
|
-
const resp = await this.requestRaw(
|
|
260
|
+
async request(path12, options = {}) {
|
|
261
|
+
const resp = await this.requestRaw(path12, options);
|
|
259
262
|
return resp.json();
|
|
260
263
|
}
|
|
261
264
|
async createIssue(title, description, labels) {
|
|
@@ -434,8 +437,8 @@ var GongfengClient = class {
|
|
|
434
437
|
}
|
|
435
438
|
return mr;
|
|
436
439
|
}
|
|
437
|
-
async requestGlobal(
|
|
438
|
-
const url = `${this.apiUrl}${
|
|
440
|
+
async requestGlobal(path12, options = {}) {
|
|
441
|
+
const url = `${this.apiUrl}${path12}`;
|
|
439
442
|
logger4.debug("API request (global)", { method: options.method || "GET", url });
|
|
440
443
|
const resp = await this.circuitBreaker.execute(
|
|
441
444
|
() => this.retryPolicy.execute(async () => {
|
|
@@ -452,7 +455,7 @@ var GongfengClient = class {
|
|
|
452
455
|
throw new GongfengApiError(r.status, `Gongfeng API error ${r.status}: ${body}`, body);
|
|
453
456
|
}
|
|
454
457
|
return r;
|
|
455
|
-
}, `requestGlobal ${options.method || "GET"} ${
|
|
458
|
+
}, `requestGlobal ${options.method || "GET"} ${path12}`)
|
|
456
459
|
);
|
|
457
460
|
return resp.json();
|
|
458
461
|
}
|
|
@@ -1646,7 +1649,7 @@ var PlanPersistence = class _PlanPersistence {
|
|
|
1646
1649
|
};
|
|
1647
1650
|
|
|
1648
1651
|
// src/phases/BasePhase.ts
|
|
1649
|
-
import
|
|
1652
|
+
import path4 from "path";
|
|
1650
1653
|
|
|
1651
1654
|
// src/rules/RuleResolver.ts
|
|
1652
1655
|
import { readdir, readFile } from "fs/promises";
|
|
@@ -1743,454 +1746,6 @@ ${rule.content}`;
|
|
|
1743
1746
|
}
|
|
1744
1747
|
};
|
|
1745
1748
|
|
|
1746
|
-
// src/hooks/HookInjector.ts
|
|
1747
|
-
import fs3 from "fs";
|
|
1748
|
-
import path4 from "path";
|
|
1749
|
-
var logger7 = logger.child("HookInjector");
|
|
1750
|
-
var HOOKS_DIR = ".claude-plan/.hooks";
|
|
1751
|
-
var EVENTS_FILE_NAME = ".hook-events.jsonl";
|
|
1752
|
-
var MANIFEST_FILE_NAME = ".artifact-manifest.jsonl";
|
|
1753
|
-
var CONTEXT_FILE_NAME = ".hook-context.json";
|
|
1754
|
-
var HookInjector = class {
|
|
1755
|
-
inject(ctx) {
|
|
1756
|
-
this.writeHookScripts(ctx);
|
|
1757
|
-
this.writeContextFile(ctx);
|
|
1758
|
-
this.writeSettingsLocal(ctx);
|
|
1759
|
-
this.initEventsFile(ctx);
|
|
1760
|
-
logger7.info("Hooks injected", {
|
|
1761
|
-
workDir: ctx.workDir,
|
|
1762
|
-
issueIid: ctx.issueIid,
|
|
1763
|
-
phase: ctx.phaseName,
|
|
1764
|
-
artifacts: ctx.expectedArtifacts
|
|
1765
|
-
});
|
|
1766
|
-
}
|
|
1767
|
-
/**
|
|
1768
|
-
* 阶段切换时更新 hooks 配置(重写脚本 + settings.local.json)。
|
|
1769
|
-
* 保留 events/manifest 文件(不截断),仅更新脚本和配置。
|
|
1770
|
-
*/
|
|
1771
|
-
updateForPhase(ctx) {
|
|
1772
|
-
this.writeHookScripts(ctx);
|
|
1773
|
-
this.writeContextFile(ctx);
|
|
1774
|
-
this.writeSettingsLocal(ctx);
|
|
1775
|
-
logger7.info("Hooks updated for phase", {
|
|
1776
|
-
workDir: ctx.workDir,
|
|
1777
|
-
issueIid: ctx.issueIid,
|
|
1778
|
-
phase: ctx.phaseName
|
|
1779
|
-
});
|
|
1780
|
-
}
|
|
1781
|
-
readManifest(workDir) {
|
|
1782
|
-
const manifestPath = path4.join(workDir, ".claude-plan", MANIFEST_FILE_NAME);
|
|
1783
|
-
return readJsonl(manifestPath);
|
|
1784
|
-
}
|
|
1785
|
-
readEvents(workDir) {
|
|
1786
|
-
const eventsPath = path4.join(workDir, ".claude-plan", EVENTS_FILE_NAME);
|
|
1787
|
-
return readJsonl(eventsPath);
|
|
1788
|
-
}
|
|
1789
|
-
getEventsFilePath(workDir) {
|
|
1790
|
-
return path4.join(workDir, ".claude-plan", EVENTS_FILE_NAME);
|
|
1791
|
-
}
|
|
1792
|
-
getManifestFilePath(workDir) {
|
|
1793
|
-
return path4.join(workDir, ".claude-plan", MANIFEST_FILE_NAME);
|
|
1794
|
-
}
|
|
1795
|
-
cleanup(workDir) {
|
|
1796
|
-
const hooksDir = path4.join(workDir, HOOKS_DIR);
|
|
1797
|
-
try {
|
|
1798
|
-
if (fs3.existsSync(hooksDir)) {
|
|
1799
|
-
fs3.rmSync(hooksDir, { recursive: true });
|
|
1800
|
-
}
|
|
1801
|
-
} catch (err) {
|
|
1802
|
-
logger7.warn("Failed to cleanup hooks", { error: err.message });
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
// ---------------------------------------------------------------------------
|
|
1806
|
-
// Private
|
|
1807
|
-
// ---------------------------------------------------------------------------
|
|
1808
|
-
writeHookScripts(ctx) {
|
|
1809
|
-
const hooksDir = path4.join(ctx.workDir, HOOKS_DIR);
|
|
1810
|
-
fs3.mkdirSync(hooksDir, { recursive: true });
|
|
1811
|
-
const eventsFile = path4.join(ctx.workDir, ".claude-plan", EVENTS_FILE_NAME);
|
|
1812
|
-
const manifestFile = path4.join(ctx.workDir, ".claude-plan", MANIFEST_FILE_NAME);
|
|
1813
|
-
const contextFile = path4.join(ctx.workDir, ".claude-plan", CONTEXT_FILE_NAME);
|
|
1814
|
-
const expected = ctx.expectedArtifacts.join(",");
|
|
1815
|
-
const phaseExpected = (ctx.phaseExpectedArtifacts ?? ctx.expectedArtifacts).join(",");
|
|
1816
|
-
const scripts = [
|
|
1817
|
-
{ name: "session-start.sh", content: buildSessionStartScript(eventsFile) },
|
|
1818
|
-
{ name: "compact-restore.sh", content: buildCompactRestoreScript(eventsFile, contextFile) },
|
|
1819
|
-
{ name: "post-tool-use.sh", content: buildPostToolUseScript(eventsFile, manifestFile, expected) },
|
|
1820
|
-
{ name: "post-artifact.sh", content: buildPostArtifactScript(manifestFile, expected) },
|
|
1821
|
-
{ name: "exit-plan-mode.sh", content: buildExitPlanModeScript(eventsFile) },
|
|
1822
|
-
{ name: "permission.sh", content: buildPermissionScript(eventsFile) },
|
|
1823
|
-
{ name: "protect-files.sh", content: buildProtectFilesScript(eventsFile, ctx.phaseName, ctx.planDir) },
|
|
1824
|
-
{ name: "stop.sh", content: buildStopScript(eventsFile, ctx.planDir, phaseExpected) }
|
|
1825
|
-
];
|
|
1826
|
-
for (const { name, content } of scripts) {
|
|
1827
|
-
const scriptPath = path4.join(hooksDir, name);
|
|
1828
|
-
fs3.writeFileSync(scriptPath, content, { mode: 493 });
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
writeContextFile(ctx) {
|
|
1832
|
-
const contextPath = path4.join(ctx.workDir, ".claude-plan", CONTEXT_FILE_NAME);
|
|
1833
|
-
const context = {
|
|
1834
|
-
issueIid: ctx.issueIid,
|
|
1835
|
-
issueTitle: ctx.issueTitle ?? "",
|
|
1836
|
-
issueDescription: ctx.issueDescription ?? "",
|
|
1837
|
-
phaseName: ctx.phaseName ?? "",
|
|
1838
|
-
expectedArtifacts: ctx.expectedArtifacts,
|
|
1839
|
-
planDir: ctx.planDir
|
|
1840
|
-
};
|
|
1841
|
-
fs3.writeFileSync(contextPath, JSON.stringify(context, null, 2), "utf-8");
|
|
1842
|
-
}
|
|
1843
|
-
writeSettingsLocal(ctx) {
|
|
1844
|
-
const claudeDir = path4.join(ctx.workDir, ".claude");
|
|
1845
|
-
fs3.mkdirSync(claudeDir, { recursive: true });
|
|
1846
|
-
const settingsPath = path4.join(claudeDir, "settings.local.json");
|
|
1847
|
-
let existing = {};
|
|
1848
|
-
if (fs3.existsSync(settingsPath)) {
|
|
1849
|
-
try {
|
|
1850
|
-
existing = JSON.parse(fs3.readFileSync(settingsPath, "utf-8"));
|
|
1851
|
-
} catch {
|
|
1852
|
-
logger7.warn("Failed to parse existing settings.local.json, overwriting");
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
const hooksDir = path4.join(ctx.workDir, HOOKS_DIR);
|
|
1856
|
-
const hooks = buildHooksConfig(hooksDir, ctx);
|
|
1857
|
-
const merged = { ...existing, hooks };
|
|
1858
|
-
fs3.writeFileSync(settingsPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
1859
|
-
}
|
|
1860
|
-
initEventsFile(ctx) {
|
|
1861
|
-
const eventsPath = path4.join(ctx.workDir, ".claude-plan", EVENTS_FILE_NAME);
|
|
1862
|
-
const manifestPath = path4.join(ctx.workDir, ".claude-plan", MANIFEST_FILE_NAME);
|
|
1863
|
-
fs3.writeFileSync(eventsPath, "", "utf-8");
|
|
1864
|
-
fs3.writeFileSync(manifestPath, "", "utf-8");
|
|
1865
|
-
}
|
|
1866
|
-
};
|
|
1867
|
-
function buildHooksConfig(hooksDir, ctx) {
|
|
1868
|
-
const isPlanPhase = ctx.phaseName === "plan";
|
|
1869
|
-
const artifactIfPatterns = buildArtifactIfPatterns(ctx.expectedArtifacts);
|
|
1870
|
-
const config = {
|
|
1871
|
-
SessionStart: [
|
|
1872
|
-
{
|
|
1873
|
-
hooks: [{
|
|
1874
|
-
type: "command",
|
|
1875
|
-
command: path4.join(hooksDir, "session-start.sh"),
|
|
1876
|
-
timeout: 5
|
|
1877
|
-
}]
|
|
1878
|
-
},
|
|
1879
|
-
{
|
|
1880
|
-
matcher: "compact",
|
|
1881
|
-
hooks: [{
|
|
1882
|
-
type: "command",
|
|
1883
|
-
command: path4.join(hooksDir, "compact-restore.sh"),
|
|
1884
|
-
timeout: 5
|
|
1885
|
-
}]
|
|
1886
|
-
}
|
|
1887
|
-
],
|
|
1888
|
-
PreToolUse: [
|
|
1889
|
-
{
|
|
1890
|
-
matcher: "Edit|Write",
|
|
1891
|
-
hooks: [{
|
|
1892
|
-
type: "command",
|
|
1893
|
-
command: path4.join(hooksDir, "protect-files.sh"),
|
|
1894
|
-
timeout: 5,
|
|
1895
|
-
...buildProtectIfClause(ctx.phaseName)
|
|
1896
|
-
}]
|
|
1897
|
-
}
|
|
1898
|
-
],
|
|
1899
|
-
PostToolUse: buildPostToolUseConfig(hooksDir, artifactIfPatterns),
|
|
1900
|
-
PermissionRequest: buildPermissionRequestConfig(hooksDir, isPlanPhase),
|
|
1901
|
-
Stop: [{
|
|
1902
|
-
hooks: [{
|
|
1903
|
-
type: "command",
|
|
1904
|
-
command: path4.join(hooksDir, "stop.sh"),
|
|
1905
|
-
timeout: 15
|
|
1906
|
-
}]
|
|
1907
|
-
}]
|
|
1908
|
-
};
|
|
1909
|
-
return config;
|
|
1910
|
-
}
|
|
1911
|
-
function buildPermissionRequestConfig(hooksDir, isPlanPhase) {
|
|
1912
|
-
const groups = [];
|
|
1913
|
-
if (isPlanPhase) {
|
|
1914
|
-
groups.push({
|
|
1915
|
-
matcher: "ExitPlanMode",
|
|
1916
|
-
hooks: [{
|
|
1917
|
-
type: "command",
|
|
1918
|
-
command: path4.join(hooksDir, "exit-plan-mode.sh"),
|
|
1919
|
-
timeout: 5
|
|
1920
|
-
}]
|
|
1921
|
-
});
|
|
1922
|
-
}
|
|
1923
|
-
groups.push({
|
|
1924
|
-
matcher: "Bash|Edit|Write|Read|Glob|Grep|WebFetch|WebSearch|mcp__.*",
|
|
1925
|
-
hooks: [{
|
|
1926
|
-
type: "command",
|
|
1927
|
-
command: path4.join(hooksDir, "permission.sh"),
|
|
1928
|
-
timeout: 5
|
|
1929
|
-
}]
|
|
1930
|
-
});
|
|
1931
|
-
return groups;
|
|
1932
|
-
}
|
|
1933
|
-
function buildPostToolUseConfig(hooksDir, artifactIfPatterns) {
|
|
1934
|
-
const groups = [];
|
|
1935
|
-
if (artifactIfPatterns) {
|
|
1936
|
-
groups.push({
|
|
1937
|
-
matcher: "Write|Edit",
|
|
1938
|
-
hooks: [{
|
|
1939
|
-
type: "command",
|
|
1940
|
-
command: path4.join(hooksDir, "post-artifact.sh"),
|
|
1941
|
-
timeout: 10,
|
|
1942
|
-
if: artifactIfPatterns
|
|
1943
|
-
}]
|
|
1944
|
-
});
|
|
1945
|
-
}
|
|
1946
|
-
groups.push({
|
|
1947
|
-
matcher: "Write|Edit",
|
|
1948
|
-
hooks: [{
|
|
1949
|
-
type: "command",
|
|
1950
|
-
command: path4.join(hooksDir, "post-tool-use.sh"),
|
|
1951
|
-
timeout: 10
|
|
1952
|
-
}]
|
|
1953
|
-
});
|
|
1954
|
-
return groups;
|
|
1955
|
-
}
|
|
1956
|
-
function buildArtifactIfPatterns(artifacts) {
|
|
1957
|
-
if (artifacts.length === 0) return void 0;
|
|
1958
|
-
return artifacts.flatMap((f) => [`Write(*${f})`, `Edit(*${f})`]).join("|");
|
|
1959
|
-
}
|
|
1960
|
-
function buildProtectIfClause(phaseName) {
|
|
1961
|
-
const alwaysProtected = [".env", ".env.*", "package-lock.json", "pnpm-lock.yaml"];
|
|
1962
|
-
const patterns = [...alwaysProtected];
|
|
1963
|
-
if (phaseName === "build") {
|
|
1964
|
-
patterns.push("01-plan.md");
|
|
1965
|
-
}
|
|
1966
|
-
if (phaseName === "verify") {
|
|
1967
|
-
patterns.push("01-plan.md");
|
|
1968
|
-
}
|
|
1969
|
-
const ifValue = patterns.flatMap((f) => [`Edit(*${f})`, `Write(*${f})`]).join("|");
|
|
1970
|
-
return { if: ifValue };
|
|
1971
|
-
}
|
|
1972
|
-
function buildSessionStartScript(eventsFile) {
|
|
1973
|
-
return `#!/bin/bash
|
|
1974
|
-
set -euo pipefail
|
|
1975
|
-
INPUT=$(cat)
|
|
1976
|
-
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
|
|
1977
|
-
printf '{"ts":"%s","event":"session_start","session_id":"%s"}\\n' \\
|
|
1978
|
-
"$(date -u +%FT%TZ)" "$SESSION_ID" >> ${quote(eventsFile)}
|
|
1979
|
-
exit 0
|
|
1980
|
-
`;
|
|
1981
|
-
}
|
|
1982
|
-
function buildCompactRestoreScript(eventsFile, contextFile) {
|
|
1983
|
-
return `#!/bin/bash
|
|
1984
|
-
set -euo pipefail
|
|
1985
|
-
|
|
1986
|
-
CONTEXT_FILE=${quote(contextFile)}
|
|
1987
|
-
if [ ! -f "$CONTEXT_FILE" ]; then
|
|
1988
|
-
exit 0
|
|
1989
|
-
fi
|
|
1990
|
-
|
|
1991
|
-
ISSUE_IID=$(jq -r '.issueIid // empty' < "$CONTEXT_FILE")
|
|
1992
|
-
ISSUE_TITLE=$(jq -r '.issueTitle // empty' < "$CONTEXT_FILE")
|
|
1993
|
-
ISSUE_DESC=$(jq -r '.issueDescription // empty' < "$CONTEXT_FILE")
|
|
1994
|
-
PHASE=$(jq -r '.phaseName // empty' < "$CONTEXT_FILE")
|
|
1995
|
-
PLAN_DIR=$(jq -r '.planDir // empty' < "$CONTEXT_FILE")
|
|
1996
|
-
ARTIFACTS=$(jq -r '.expectedArtifacts | join(", ") // empty' < "$CONTEXT_FILE")
|
|
1997
|
-
|
|
1998
|
-
READY=""
|
|
1999
|
-
MISSING=""
|
|
2000
|
-
for f in $(jq -r '.expectedArtifacts[]' < "$CONTEXT_FILE" 2>/dev/null); do
|
|
2001
|
-
FPATH="$PLAN_DIR/$f"
|
|
2002
|
-
if [ -f "$FPATH" ] && [ "$(wc -c < "$FPATH")" -ge 50 ]; then
|
|
2003
|
-
READY="$READY $f"
|
|
2004
|
-
else
|
|
2005
|
-
MISSING="$MISSING $f"
|
|
2006
|
-
fi
|
|
2007
|
-
done
|
|
2008
|
-
READY=$(echo "$READY" | xargs)
|
|
2009
|
-
MISSING=$(echo "$MISSING" | xargs)
|
|
2010
|
-
|
|
2011
|
-
printf '{"ts":"%s","event":"compact_restore"}\\n' "$(date -u +%FT%TZ)" >> ${quote(eventsFile)}
|
|
2012
|
-
|
|
2013
|
-
cat <<CONTEXT
|
|
2014
|
-
[\u4E0A\u4E0B\u6587\u6062\u590D \u2014 compaction \u540E\u81EA\u52A8\u6CE8\u5165]
|
|
2015
|
-
Issue #$ISSUE_IID: $ISSUE_TITLE
|
|
2016
|
-
\u5F53\u524D\u9636\u6BB5: $PHASE
|
|
2017
|
-
\u9884\u671F\u4EA7\u7269: $ARTIFACTS
|
|
2018
|
-
\u5DF2\u5C31\u7EEA: \${READY:-\u65E0}
|
|
2019
|
-
\u672A\u5B8C\u6210: \${MISSING:-\u65E0}
|
|
2020
|
-
|
|
2021
|
-
\u9700\u6C42\u63CF\u8FF0:
|
|
2022
|
-
$ISSUE_DESC
|
|
2023
|
-
CONTEXT
|
|
2024
|
-
exit 0
|
|
2025
|
-
`;
|
|
2026
|
-
}
|
|
2027
|
-
function buildPostToolUseScript(eventsFile, manifestFile, expected) {
|
|
2028
|
-
return `#!/bin/bash
|
|
2029
|
-
set -euo pipefail
|
|
2030
|
-
INPUT=$(cat)
|
|
2031
|
-
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
2032
|
-
[ -z "$FILE_PATH" ] && exit 0
|
|
2033
|
-
|
|
2034
|
-
EXPECTED=${quote(expected)}
|
|
2035
|
-
BASENAME=$(basename "$FILE_PATH")
|
|
2036
|
-
|
|
2037
|
-
if echo "$EXPECTED" | tr ',' '\\n' | grep -qx "$BASENAME"; then
|
|
2038
|
-
BYTES=$(wc -c < "$FILE_PATH" 2>/dev/null || echo 0)
|
|
2039
|
-
printf '{"ts":"%s","event":"artifact_write","file":"%s","path":"%s","bytes":%s}\\n' \\
|
|
2040
|
-
"$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" "$BYTES" >> ${quote(manifestFile)}
|
|
2041
|
-
fi
|
|
2042
|
-
|
|
2043
|
-
printf '{"ts":"%s","event":"artifact_write","file":"%s","path":"%s","bytes":0}\\n' \\
|
|
2044
|
-
"$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" >> ${quote(eventsFile)}
|
|
2045
|
-
exit 0
|
|
2046
|
-
`;
|
|
2047
|
-
}
|
|
2048
|
-
function buildPostArtifactScript(manifestFile, expected) {
|
|
2049
|
-
return `#!/bin/bash
|
|
2050
|
-
set -euo pipefail
|
|
2051
|
-
INPUT=$(cat)
|
|
2052
|
-
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
2053
|
-
[ -z "$FILE_PATH" ] && exit 0
|
|
2054
|
-
|
|
2055
|
-
EXPECTED=${quote(expected)}
|
|
2056
|
-
BASENAME=$(basename "$FILE_PATH")
|
|
2057
|
-
|
|
2058
|
-
if echo "$EXPECTED" | tr ',' '\\n' | grep -qx "$BASENAME"; then
|
|
2059
|
-
BYTES=$(wc -c < "$FILE_PATH" 2>/dev/null || echo 0)
|
|
2060
|
-
printf '{"ts":"%s","event":"write","file":"%s","path":"%s","bytes":%s}\\n' \\
|
|
2061
|
-
"$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" "$BYTES" >> ${quote(manifestFile)}
|
|
2062
|
-
fi
|
|
2063
|
-
exit 0
|
|
2064
|
-
`;
|
|
2065
|
-
}
|
|
2066
|
-
function buildExitPlanModeScript(eventsFile) {
|
|
2067
|
-
return `#!/bin/bash
|
|
2068
|
-
set -euo pipefail
|
|
2069
|
-
INPUT=$(cat)
|
|
2070
|
-
|
|
2071
|
-
printf '{"ts":"%s","event":"exit_plan_mode"}\\n' "$(date -u +%FT%TZ)" >> ${quote(eventsFile)}
|
|
2072
|
-
|
|
2073
|
-
echo '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
|
|
2074
|
-
exit 0
|
|
2075
|
-
`;
|
|
2076
|
-
}
|
|
2077
|
-
function buildPermissionScript(eventsFile) {
|
|
2078
|
-
return `#!/bin/bash
|
|
2079
|
-
set -euo pipefail
|
|
2080
|
-
INPUT=$(cat)
|
|
2081
|
-
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
|
|
2082
|
-
printf '{"ts":"%s","event":"permission_request","tool":"%s"}\\n' \\
|
|
2083
|
-
"$(date -u +%FT%TZ)" "$TOOL" >> ${quote(eventsFile)}
|
|
2084
|
-
echo '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
|
|
2085
|
-
exit 0
|
|
2086
|
-
`;
|
|
2087
|
-
}
|
|
2088
|
-
function buildProtectFilesScript(eventsFile, phaseName, planDir) {
|
|
2089
|
-
return `#!/bin/bash
|
|
2090
|
-
set -euo pipefail
|
|
2091
|
-
INPUT=$(cat)
|
|
2092
|
-
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
|
|
2093
|
-
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
2094
|
-
[ -z "$FILE_PATH" ] && exit 0
|
|
2095
|
-
|
|
2096
|
-
BASENAME=$(basename "$FILE_PATH")
|
|
2097
|
-
PHASE=${quote(phaseName ?? "")}
|
|
2098
|
-
|
|
2099
|
-
blocked_reason() {
|
|
2100
|
-
printf '{"ts":"%s","event":"protect_blocked","tool":"%s","file":"%s"}\\n' \\
|
|
2101
|
-
"$(date -u +%FT%TZ)" "$TOOL" "$BASENAME" >> ${quote(eventsFile)}
|
|
2102
|
-
echo "$1" >&2
|
|
2103
|
-
exit 2
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
case "$BASENAME" in
|
|
2107
|
-
.env|.env.*)
|
|
2108
|
-
blocked_reason "\u7981\u6B62\u4FEE\u6539\u73AF\u5883\u914D\u7F6E\u6587\u4EF6 $BASENAME\uFF0C\u8BF7\u901A\u8FC7 .env.example \u6216\u6587\u6863\u8BF4\u660E\u914D\u7F6E\u53D8\u66F4\u3002"
|
|
2109
|
-
;;
|
|
2110
|
-
package-lock.json|pnpm-lock.yaml)
|
|
2111
|
-
blocked_reason "\u7981\u6B62\u76F4\u63A5\u4FEE\u6539\u9501\u6587\u4EF6 $BASENAME\uFF0C\u8BF7\u901A\u8FC7 npm install / pnpm install \u66F4\u65B0\u4F9D\u8D56\u3002"
|
|
2112
|
-
;;
|
|
2113
|
-
esac
|
|
2114
|
-
|
|
2115
|
-
if [ "$PHASE" = "build" ] || [ "$PHASE" = "verify" ]; then
|
|
2116
|
-
case "$BASENAME" in
|
|
2117
|
-
01-plan.md)
|
|
2118
|
-
blocked_reason "\u5728 $PHASE \u9636\u6BB5\u7981\u6B62\u4FEE\u6539\u89C4\u5212\u6587\u6863 01-plan.md\uFF0C\u8BE5\u6587\u4EF6\u5728 plan \u9636\u6BB5\u5DF2\u786E\u5B9A\u3002"
|
|
2119
|
-
;;
|
|
2120
|
-
esac
|
|
2121
|
-
fi
|
|
2122
|
-
|
|
2123
|
-
exit 0
|
|
2124
|
-
`;
|
|
2125
|
-
}
|
|
2126
|
-
function buildStopScript(eventsFile, planDir, phaseExpected) {
|
|
2127
|
-
return `#!/bin/bash
|
|
2128
|
-
set -euo pipefail
|
|
2129
|
-
INPUT=$(cat)
|
|
2130
|
-
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
|
|
2131
|
-
|
|
2132
|
-
PLAN_DIR=${quote(planDir)}
|
|
2133
|
-
MIN_BYTES=50
|
|
2134
|
-
PHASE_EXPECTED=${quote(phaseExpected)}
|
|
2135
|
-
|
|
2136
|
-
MISSING=""
|
|
2137
|
-
READY=""
|
|
2138
|
-
for f in $(echo "$PHASE_EXPECTED" | tr ',' ' '); do
|
|
2139
|
-
[ -z "$f" ] && continue
|
|
2140
|
-
FPATH="$PLAN_DIR/$f"
|
|
2141
|
-
if [ -f "$FPATH" ] && [ "$(wc -c < "$FPATH")" -ge "$MIN_BYTES" ]; then
|
|
2142
|
-
BYTES=$(wc -c < "$FPATH")
|
|
2143
|
-
READY="$READY $f(\${BYTES} bytes)"
|
|
2144
|
-
else
|
|
2145
|
-
MISSING="$MISSING $f"
|
|
2146
|
-
fi
|
|
2147
|
-
done
|
|
2148
|
-
|
|
2149
|
-
MISSING=$(echo "$MISSING" | xargs)
|
|
2150
|
-
READY=$(echo "$READY" | xargs)
|
|
2151
|
-
|
|
2152
|
-
if [ -n "$MISSING" ] && [ "$STOP_ACTIVE" != "true" ]; then
|
|
2153
|
-
printf '{"ts":"%s","event":"stop","blocked":true,"missing":"%s"}\\n' \\
|
|
2154
|
-
"$(date -u +%FT%TZ)" "$MISSING" >> ${quote(eventsFile)}
|
|
2155
|
-
|
|
2156
|
-
REASON="\u4EA7\u7269\u672A\u5C31\u7EEA: $MISSING\u3002\u8BF7\u5199\u5165 $PLAN_DIR/ \u4E0B\u7684\u5BF9\u5E94\u6587\u4EF6\u3002\u5DF2\u5C31\u7EEA: \${READY:-\u65E0}"
|
|
2157
|
-
|
|
2158
|
-
printf '{"decision":"block","reason":"%s"}' "$REASON"
|
|
2159
|
-
exit 0
|
|
2160
|
-
fi
|
|
2161
|
-
|
|
2162
|
-
printf '{"ts":"%s","event":"stop","blocked":false,"missing":"%s"}\\n' \\
|
|
2163
|
-
"$(date -u +%FT%TZ)" "\${MISSING:-none}" >> ${quote(eventsFile)}
|
|
2164
|
-
exit 0
|
|
2165
|
-
`;
|
|
2166
|
-
}
|
|
2167
|
-
function quote(s) {
|
|
2168
|
-
return `"${s.replace(/"/g, '\\"')}"`;
|
|
2169
|
-
}
|
|
2170
|
-
function readJsonl(filePath) {
|
|
2171
|
-
if (!fs3.existsSync(filePath)) return [];
|
|
2172
|
-
try {
|
|
2173
|
-
const content = fs3.readFileSync(filePath, "utf-8").trim();
|
|
2174
|
-
if (!content) return [];
|
|
2175
|
-
return content.split("\n").reduce((acc, line) => {
|
|
2176
|
-
const trimmed = line.trim();
|
|
2177
|
-
if (!trimmed) return acc;
|
|
2178
|
-
try {
|
|
2179
|
-
acc.push(JSON.parse(trimmed));
|
|
2180
|
-
} catch {
|
|
2181
|
-
logger7.debug("Skipping malformed JSONL line", { line: trimmed });
|
|
2182
|
-
}
|
|
2183
|
-
return acc;
|
|
2184
|
-
}, []);
|
|
2185
|
-
} catch {
|
|
2186
|
-
return [];
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
// src/hooks/HookEventWatcher.ts
|
|
2191
|
-
import fs4 from "fs";
|
|
2192
|
-
var logger8 = logger.child("HookEventWatcher");
|
|
2193
|
-
|
|
2194
1749
|
// src/phases/BasePhase.ts
|
|
2195
1750
|
var BasePhase = class _BasePhase {
|
|
2196
1751
|
static MIN_ARTIFACT_BYTES = 50;
|
|
@@ -2335,7 +1890,7 @@ ${t("basePhase.rulesSection", { rules })}`;
|
|
|
2335
1890
|
const resultFiles = this.getResultFiles(ctx);
|
|
2336
1891
|
const snapshotFilenames = resultFiles.map((f) => f.filename);
|
|
2337
1892
|
const artifactCheck = snapshotFilenames.length > 0 ? () => snapshotFilenames.every((fn) => this.plan.isArtifactReady(fn, _BasePhase.MIN_ARTIFACT_BYTES)) : void 0;
|
|
2338
|
-
const artifactPaths = snapshotFilenames.length > 0 ? snapshotFilenames.map((fn) =>
|
|
1893
|
+
const artifactPaths = snapshotFilenames.length > 0 ? snapshotFilenames.map((fn) => path4.join(this.plan.planDir, fn)) : void 0;
|
|
2339
1894
|
let capturedSessionId;
|
|
2340
1895
|
const result = await this.aiRunner.run({
|
|
2341
1896
|
prompt,
|
|
@@ -2432,14 +1987,14 @@ ${t("basePhase.rulesSection", { rules })}`;
|
|
|
2432
1987
|
const context = `${ctx.demand.title} ${ctx.demand.description} ${ctx.demand.supplement ? JSON.stringify(ctx.demand.supplement) : ""}`;
|
|
2433
1988
|
if (ctx.workspace && ctx.workspace.repos.length > 1) {
|
|
2434
1989
|
for (const repo of ctx.workspace.repos) {
|
|
2435
|
-
const rulesDir =
|
|
1990
|
+
const rulesDir = path4.join(repo.gitRootDir, ".cursor", "rules");
|
|
2436
1991
|
try {
|
|
2437
1992
|
await resolver.loadRules(rulesDir);
|
|
2438
1993
|
} catch {
|
|
2439
1994
|
}
|
|
2440
1995
|
}
|
|
2441
1996
|
} else {
|
|
2442
|
-
const rulesDir =
|
|
1997
|
+
const rulesDir = path4.join(this.plan.baseDir, ".cursor", "rules");
|
|
2443
1998
|
await resolver.loadRules(rulesDir);
|
|
2444
1999
|
}
|
|
2445
2000
|
const matched = resolver.matchRules(context);
|
|
@@ -2713,20 +2268,20 @@ var BuildPhase = class extends BasePhase {
|
|
|
2713
2268
|
};
|
|
2714
2269
|
|
|
2715
2270
|
// src/release/ReleaseDetectCache.ts
|
|
2716
|
-
import
|
|
2717
|
-
import
|
|
2271
|
+
import fs3 from "fs";
|
|
2272
|
+
import path5 from "path";
|
|
2718
2273
|
import { createHash } from "crypto";
|
|
2719
|
-
var
|
|
2274
|
+
var logger7 = logger.child("ReleaseDetectCache");
|
|
2720
2275
|
function hashProjectPath(projectPath) {
|
|
2721
2276
|
return createHash("sha256").update(projectPath).digest("hex").slice(0, 16);
|
|
2722
2277
|
}
|
|
2723
2278
|
var ReleaseDetectCache = class {
|
|
2724
2279
|
cacheDir;
|
|
2725
2280
|
constructor(dataDir) {
|
|
2726
|
-
this.cacheDir =
|
|
2281
|
+
this.cacheDir = path5.join(dataDir, "release-detect");
|
|
2727
2282
|
}
|
|
2728
2283
|
filePath(projectPath) {
|
|
2729
|
-
return
|
|
2284
|
+
return path5.join(this.cacheDir, `${hashProjectPath(projectPath)}.json`);
|
|
2730
2285
|
}
|
|
2731
2286
|
/**
|
|
2732
2287
|
* 读取缓存。返回 null 如果不存在、已过期或校验失败。
|
|
@@ -2734,21 +2289,21 @@ var ReleaseDetectCache = class {
|
|
|
2734
2289
|
get(projectPath, ttlMs) {
|
|
2735
2290
|
const fp = this.filePath(projectPath);
|
|
2736
2291
|
try {
|
|
2737
|
-
if (!
|
|
2738
|
-
const raw =
|
|
2292
|
+
if (!fs3.existsSync(fp)) return null;
|
|
2293
|
+
const raw = fs3.readFileSync(fp, "utf-8");
|
|
2739
2294
|
const data = JSON.parse(raw);
|
|
2740
2295
|
if (data.projectPath !== projectPath) {
|
|
2741
|
-
|
|
2296
|
+
logger7.warn("Cache projectPath mismatch, ignoring", { expected: projectPath, got: data.projectPath });
|
|
2742
2297
|
return null;
|
|
2743
2298
|
}
|
|
2744
2299
|
const age = Date.now() - new Date(data.detectedAt).getTime();
|
|
2745
2300
|
if (age > ttlMs) {
|
|
2746
|
-
|
|
2301
|
+
logger7.debug("Cache expired", { projectPath, ageMs: age, ttlMs });
|
|
2747
2302
|
return null;
|
|
2748
2303
|
}
|
|
2749
2304
|
return data;
|
|
2750
2305
|
} catch (err) {
|
|
2751
|
-
|
|
2306
|
+
logger7.warn("Failed to read release detect cache", { path: fp, error: err.message });
|
|
2752
2307
|
return null;
|
|
2753
2308
|
}
|
|
2754
2309
|
}
|
|
@@ -2758,13 +2313,13 @@ var ReleaseDetectCache = class {
|
|
|
2758
2313
|
set(result) {
|
|
2759
2314
|
const fp = this.filePath(result.projectPath);
|
|
2760
2315
|
try {
|
|
2761
|
-
if (!
|
|
2762
|
-
|
|
2316
|
+
if (!fs3.existsSync(this.cacheDir)) {
|
|
2317
|
+
fs3.mkdirSync(this.cacheDir, { recursive: true });
|
|
2763
2318
|
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2319
|
+
fs3.writeFileSync(fp, JSON.stringify(result, null, 2), "utf-8");
|
|
2320
|
+
logger7.debug("Release detect cache written", { projectPath: result.projectPath, path: fp });
|
|
2766
2321
|
} catch (err) {
|
|
2767
|
-
|
|
2322
|
+
logger7.warn("Failed to write release detect cache", { path: fp, error: err.message });
|
|
2768
2323
|
}
|
|
2769
2324
|
}
|
|
2770
2325
|
/**
|
|
@@ -2773,14 +2328,14 @@ var ReleaseDetectCache = class {
|
|
|
2773
2328
|
invalidate(projectPath) {
|
|
2774
2329
|
const fp = this.filePath(projectPath);
|
|
2775
2330
|
try {
|
|
2776
|
-
if (
|
|
2777
|
-
|
|
2778
|
-
|
|
2331
|
+
if (fs3.existsSync(fp)) {
|
|
2332
|
+
fs3.unlinkSync(fp);
|
|
2333
|
+
logger7.info("Release detect cache invalidated", { projectPath });
|
|
2779
2334
|
return true;
|
|
2780
2335
|
}
|
|
2781
2336
|
return false;
|
|
2782
2337
|
} catch (err) {
|
|
2783
|
-
|
|
2338
|
+
logger7.warn("Failed to invalidate release detect cache", { path: fp, error: err.message });
|
|
2784
2339
|
return false;
|
|
2785
2340
|
}
|
|
2786
2341
|
}
|
|
@@ -3231,9 +2786,9 @@ function createLifecycleManager(def) {
|
|
|
3231
2786
|
|
|
3232
2787
|
// src/workspace/WorkspaceConfig.ts
|
|
3233
2788
|
import { z } from "zod";
|
|
3234
|
-
import
|
|
3235
|
-
import
|
|
3236
|
-
var
|
|
2789
|
+
import fs4 from "fs";
|
|
2790
|
+
import path6 from "path";
|
|
2791
|
+
var logger8 = logger.child("WorkspaceConfig");
|
|
3237
2792
|
var repoConfigSchema = z.object({
|
|
3238
2793
|
name: z.string().min(1, "Repo name is required"),
|
|
3239
2794
|
projectPath: z.string().min(1, "Gongfeng project path is required"),
|
|
@@ -3249,29 +2804,29 @@ var workspaceConfigSchema = z.object({
|
|
|
3249
2804
|
});
|
|
3250
2805
|
function loadWorkspaceConfig(configPath) {
|
|
3251
2806
|
if (!configPath) return null;
|
|
3252
|
-
if (!
|
|
3253
|
-
|
|
2807
|
+
if (!fs4.existsSync(configPath)) {
|
|
2808
|
+
logger8.warn("Workspace config file not found, falling back to single-repo mode", {
|
|
3254
2809
|
path: configPath
|
|
3255
2810
|
});
|
|
3256
2811
|
return null;
|
|
3257
2812
|
}
|
|
3258
2813
|
try {
|
|
3259
|
-
const raw =
|
|
2814
|
+
const raw = fs4.readFileSync(configPath, "utf-8");
|
|
3260
2815
|
const json = JSON.parse(raw);
|
|
3261
2816
|
const result = workspaceConfigSchema.safeParse(json);
|
|
3262
2817
|
if (!result.success) {
|
|
3263
2818
|
const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
3264
|
-
|
|
2819
|
+
logger8.error(`Workspace config validation failed:
|
|
3265
2820
|
${issues}`);
|
|
3266
2821
|
return null;
|
|
3267
2822
|
}
|
|
3268
|
-
|
|
2823
|
+
logger8.info("Workspace config loaded", {
|
|
3269
2824
|
primary: result.data.primary.name,
|
|
3270
2825
|
associates: result.data.associates.map((a) => a.name)
|
|
3271
2826
|
});
|
|
3272
2827
|
return result.data;
|
|
3273
2828
|
} catch (err) {
|
|
3274
|
-
|
|
2829
|
+
logger8.error("Failed to parse workspace config", {
|
|
3275
2830
|
path: configPath,
|
|
3276
2831
|
error: err.message
|
|
3277
2832
|
});
|
|
@@ -3297,14 +2852,14 @@ function isMultiRepo(ws) {
|
|
|
3297
2852
|
}
|
|
3298
2853
|
function persistWorkspaceConfig(ws, filePath) {
|
|
3299
2854
|
try {
|
|
3300
|
-
const dir =
|
|
3301
|
-
if (!
|
|
3302
|
-
|
|
2855
|
+
const dir = path6.dirname(filePath);
|
|
2856
|
+
if (!fs4.existsSync(dir)) {
|
|
2857
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
3303
2858
|
}
|
|
3304
|
-
|
|
3305
|
-
|
|
2859
|
+
fs4.writeFileSync(filePath, JSON.stringify(ws, null, 2) + "\n", "utf-8");
|
|
2860
|
+
logger8.info("Workspace config auto-generated from .env", { path: filePath });
|
|
3306
2861
|
} catch (err) {
|
|
3307
|
-
|
|
2862
|
+
logger8.warn("Failed to persist workspace config", {
|
|
3308
2863
|
path: filePath,
|
|
3309
2864
|
error: err.message
|
|
3310
2865
|
});
|
|
@@ -3312,12 +2867,12 @@ function persistWorkspaceConfig(ws, filePath) {
|
|
|
3312
2867
|
}
|
|
3313
2868
|
|
|
3314
2869
|
// src/workspace/WorkspaceManager.ts
|
|
3315
|
-
import
|
|
3316
|
-
import
|
|
2870
|
+
import path7 from "path";
|
|
2871
|
+
import fs5 from "fs/promises";
|
|
3317
2872
|
import { execFile } from "child_process";
|
|
3318
2873
|
import { promisify } from "util";
|
|
3319
2874
|
var execFileAsync = promisify(execFile);
|
|
3320
|
-
var
|
|
2875
|
+
var logger9 = logger.child("WorkspaceManager");
|
|
3321
2876
|
var WorkspaceManager = class {
|
|
3322
2877
|
wsConfig;
|
|
3323
2878
|
worktreeBaseDir;
|
|
@@ -3346,7 +2901,7 @@ var WorkspaceManager = class {
|
|
|
3346
2901
|
*/
|
|
3347
2902
|
async prepareWorkspace(issueIid, branchName, globalBaseBranch, globalBranchPrefix) {
|
|
3348
2903
|
const wsRoot = this.getWorkspaceRoot(issueIid);
|
|
3349
|
-
await
|
|
2904
|
+
await fs5.mkdir(wsRoot, { recursive: true });
|
|
3350
2905
|
const primaryCtx = await this.preparePrimaryRepo(
|
|
3351
2906
|
issueIid,
|
|
3352
2907
|
branchName,
|
|
@@ -3365,7 +2920,7 @@ var WorkspaceManager = class {
|
|
|
3365
2920
|
);
|
|
3366
2921
|
associateCtxs.push(ctx);
|
|
3367
2922
|
}
|
|
3368
|
-
|
|
2923
|
+
logger9.info("Workspace prepared", {
|
|
3369
2924
|
issueIid,
|
|
3370
2925
|
wsRoot,
|
|
3371
2926
|
repos: [primaryCtx.name, ...associateCtxs.map((a) => a.name)]
|
|
@@ -3390,7 +2945,7 @@ var WorkspaceManager = class {
|
|
|
3390
2945
|
await git.commit(message);
|
|
3391
2946
|
await git.push(wsCtx.branchName);
|
|
3392
2947
|
committed.push(repo.name);
|
|
3393
|
-
|
|
2948
|
+
logger9.info("Committed and pushed changes", {
|
|
3394
2949
|
repo: repo.name,
|
|
3395
2950
|
branch: wsCtx.branchName
|
|
3396
2951
|
});
|
|
@@ -3404,19 +2959,19 @@ var WorkspaceManager = class {
|
|
|
3404
2959
|
async cleanupWorkspace(wsCtx) {
|
|
3405
2960
|
try {
|
|
3406
2961
|
await this.mainGit.worktreeRemove(wsCtx.primary.gitRootDir, true);
|
|
3407
|
-
|
|
2962
|
+
logger9.info("Primary worktree removed", { dir: wsCtx.primary.gitRootDir });
|
|
3408
2963
|
} catch (err) {
|
|
3409
|
-
|
|
2964
|
+
logger9.warn("Failed to remove primary worktree", {
|
|
3410
2965
|
dir: wsCtx.primary.gitRootDir,
|
|
3411
2966
|
error: err.message
|
|
3412
2967
|
});
|
|
3413
2968
|
}
|
|
3414
2969
|
for (const assoc of wsCtx.associates) {
|
|
3415
2970
|
try {
|
|
3416
|
-
await
|
|
3417
|
-
|
|
2971
|
+
await fs5.rm(assoc.gitRootDir, { recursive: true, force: true });
|
|
2972
|
+
logger9.info("Associate repo dir removed", { name: assoc.name, dir: assoc.gitRootDir });
|
|
3418
2973
|
} catch (err) {
|
|
3419
|
-
|
|
2974
|
+
logger9.warn("Failed to remove associate repo dir", {
|
|
3420
2975
|
name: assoc.name,
|
|
3421
2976
|
dir: assoc.gitRootDir,
|
|
3422
2977
|
error: err.message
|
|
@@ -3424,9 +2979,9 @@ var WorkspaceManager = class {
|
|
|
3424
2979
|
}
|
|
3425
2980
|
}
|
|
3426
2981
|
try {
|
|
3427
|
-
const entries = await
|
|
2982
|
+
const entries = await fs5.readdir(wsCtx.workspaceRoot);
|
|
3428
2983
|
if (entries.length === 0) {
|
|
3429
|
-
await
|
|
2984
|
+
await fs5.rmdir(wsCtx.workspaceRoot);
|
|
3430
2985
|
}
|
|
3431
2986
|
} catch {
|
|
3432
2987
|
}
|
|
@@ -3438,13 +2993,13 @@ var WorkspaceManager = class {
|
|
|
3438
2993
|
const wsRoot = this.getWorkspaceRoot(issueIid);
|
|
3439
2994
|
const primary = this.wsConfig.primary;
|
|
3440
2995
|
const defaultPrefix = globalBranchPrefix ?? primary.branchPrefix ?? "feat/issue";
|
|
3441
|
-
const primaryDir =
|
|
2996
|
+
const primaryDir = path7.join(wsRoot, primary.name);
|
|
3442
2997
|
const repos = [{
|
|
3443
2998
|
name: primary.name,
|
|
3444
2999
|
projectPath: primary.projectPath,
|
|
3445
3000
|
role: primary.role ?? "",
|
|
3446
3001
|
gitRootDir: primaryDir,
|
|
3447
|
-
workDir:
|
|
3002
|
+
workDir: path7.join(primaryDir, primary.projectSubDir ?? ""),
|
|
3448
3003
|
baseBranch: primary.baseBranch ?? globalBaseBranch,
|
|
3449
3004
|
branchPrefix: primary.branchPrefix ?? defaultPrefix,
|
|
3450
3005
|
isPrimary: true
|
|
@@ -3454,8 +3009,8 @@ var WorkspaceManager = class {
|
|
|
3454
3009
|
name: assoc.name,
|
|
3455
3010
|
projectPath: assoc.projectPath,
|
|
3456
3011
|
role: assoc.role ?? "",
|
|
3457
|
-
gitRootDir:
|
|
3458
|
-
workDir:
|
|
3012
|
+
gitRootDir: path7.join(wsRoot, assoc.name),
|
|
3013
|
+
workDir: path7.join(wsRoot, assoc.name, assoc.projectSubDir ?? ""),
|
|
3459
3014
|
baseBranch: assoc.baseBranch ?? globalBaseBranch,
|
|
3460
3015
|
branchPrefix: assoc.branchPrefix ?? defaultPrefix,
|
|
3461
3016
|
isPrimary: false
|
|
@@ -3464,12 +3019,12 @@ var WorkspaceManager = class {
|
|
|
3464
3019
|
return repos;
|
|
3465
3020
|
}
|
|
3466
3021
|
getWorkspaceRoot(issueIid) {
|
|
3467
|
-
return
|
|
3022
|
+
return path7.join(this.worktreeBaseDir, `issue-${issueIid}`);
|
|
3468
3023
|
}
|
|
3469
3024
|
// ── Internal helpers ──
|
|
3470
3025
|
async preparePrimaryRepo(issueIid, branchName, wsRoot, globalBaseBranch) {
|
|
3471
3026
|
const primary = this.wsConfig.primary;
|
|
3472
|
-
const repoDir =
|
|
3027
|
+
const repoDir = path7.join(wsRoot, primary.name);
|
|
3473
3028
|
const baseBranch = primary.baseBranch ?? globalBaseBranch;
|
|
3474
3029
|
await this.ensurePrimaryWorktree(repoDir, branchName, baseBranch);
|
|
3475
3030
|
return {
|
|
@@ -3477,18 +3032,18 @@ var WorkspaceManager = class {
|
|
|
3477
3032
|
projectPath: primary.projectPath,
|
|
3478
3033
|
role: primary.role ?? "",
|
|
3479
3034
|
gitRootDir: repoDir,
|
|
3480
|
-
workDir:
|
|
3035
|
+
workDir: path7.join(repoDir, primary.projectSubDir ?? ""),
|
|
3481
3036
|
baseBranch,
|
|
3482
3037
|
branchPrefix: primary.branchPrefix ?? "feat/issue",
|
|
3483
3038
|
isPrimary: true
|
|
3484
3039
|
};
|
|
3485
3040
|
}
|
|
3486
3041
|
async ensurePrimaryWorktree(repoDir, branchName, baseBranch) {
|
|
3487
|
-
const wsRoot =
|
|
3042
|
+
const wsRoot = path7.dirname(repoDir);
|
|
3488
3043
|
if (wsRoot !== repoDir) {
|
|
3489
3044
|
try {
|
|
3490
|
-
await
|
|
3491
|
-
|
|
3045
|
+
await fs5.access(path7.join(wsRoot, ".git"));
|
|
3046
|
+
logger9.info("Migrating legacy worktree to primary subdir", { from: wsRoot, to: repoDir });
|
|
3492
3047
|
await this.mainGit.worktreeRemove(wsRoot, true);
|
|
3493
3048
|
await this.mainGit.worktreePrune();
|
|
3494
3049
|
await this.cleanStaleDir(wsRoot);
|
|
@@ -3498,11 +3053,11 @@ var WorkspaceManager = class {
|
|
|
3498
3053
|
const worktrees = await this.mainGit.worktreeList();
|
|
3499
3054
|
if (worktrees.includes(repoDir)) {
|
|
3500
3055
|
try {
|
|
3501
|
-
await
|
|
3502
|
-
|
|
3056
|
+
await fs5.access(path7.join(repoDir, ".git"));
|
|
3057
|
+
logger9.info("Reusing existing primary worktree", { dir: repoDir });
|
|
3503
3058
|
return;
|
|
3504
3059
|
} catch {
|
|
3505
|
-
|
|
3060
|
+
logger9.warn("Primary worktree registered but .git missing, recreating", { dir: repoDir });
|
|
3506
3061
|
await this.mainGit.worktreeRemove(repoDir, true);
|
|
3507
3062
|
await this.mainGit.worktreePrune();
|
|
3508
3063
|
}
|
|
@@ -3521,19 +3076,19 @@ var WorkspaceManager = class {
|
|
|
3521
3076
|
await this.mainGit.worktreeAdd(repoDir, branchName, `origin/${baseBranch}`);
|
|
3522
3077
|
}
|
|
3523
3078
|
async prepareAssociateRepo(assoc, _issueIid, branchName, wsRoot, globalBaseBranch, globalBranchPrefix) {
|
|
3524
|
-
const repoDir =
|
|
3079
|
+
const repoDir = path7.join(wsRoot, assoc.name);
|
|
3525
3080
|
const baseBranch = assoc.baseBranch ?? globalBaseBranch;
|
|
3526
3081
|
const cloneUrl = `${this.gongfengApiUrl}/${assoc.projectPath}.git`;
|
|
3527
|
-
const gitDirExists = await this.dirExists(
|
|
3082
|
+
const gitDirExists = await this.dirExists(path7.join(repoDir, ".git"));
|
|
3528
3083
|
if (!gitDirExists) {
|
|
3529
3084
|
await this.cleanStaleDir(repoDir);
|
|
3530
|
-
|
|
3085
|
+
logger9.info("Cloning associate repo", { name: assoc.name, url: cloneUrl });
|
|
3531
3086
|
await execFileAsync("git", ["clone", "--depth", "50", cloneUrl, repoDir], {
|
|
3532
3087
|
timeout: 3e5,
|
|
3533
3088
|
maxBuffer: 10 * 1024 * 1024
|
|
3534
3089
|
});
|
|
3535
3090
|
} else {
|
|
3536
|
-
|
|
3091
|
+
logger9.info("Reusing existing associate clone", { name: assoc.name, dir: repoDir });
|
|
3537
3092
|
}
|
|
3538
3093
|
const assocGit = new GitOperations(repoDir);
|
|
3539
3094
|
await assocGit.fetch();
|
|
@@ -3556,7 +3111,7 @@ var WorkspaceManager = class {
|
|
|
3556
3111
|
projectPath: assoc.projectPath,
|
|
3557
3112
|
role: assoc.role ?? "",
|
|
3558
3113
|
gitRootDir: repoDir,
|
|
3559
|
-
workDir:
|
|
3114
|
+
workDir: path7.join(repoDir, assoc.projectSubDir ?? ""),
|
|
3560
3115
|
baseBranch,
|
|
3561
3116
|
branchPrefix: assoc.branchPrefix ?? globalBranchPrefix,
|
|
3562
3117
|
isPrimary: false
|
|
@@ -3564,13 +3119,13 @@ var WorkspaceManager = class {
|
|
|
3564
3119
|
}
|
|
3565
3120
|
async cleanStaleDir(dir) {
|
|
3566
3121
|
if (await this.dirExists(dir)) {
|
|
3567
|
-
|
|
3568
|
-
await
|
|
3122
|
+
logger9.warn("Removing stale directory", { dir });
|
|
3123
|
+
await fs5.rm(dir, { recursive: true, force: true });
|
|
3569
3124
|
}
|
|
3570
3125
|
}
|
|
3571
3126
|
async dirExists(dir) {
|
|
3572
3127
|
try {
|
|
3573
|
-
await
|
|
3128
|
+
await fs5.access(dir);
|
|
3574
3129
|
return true;
|
|
3575
3130
|
} catch {
|
|
3576
3131
|
return false;
|
|
@@ -3579,8 +3134,8 @@ var WorkspaceManager = class {
|
|
|
3579
3134
|
};
|
|
3580
3135
|
|
|
3581
3136
|
// src/orchestrator/PipelineOrchestrator.ts
|
|
3582
|
-
import
|
|
3583
|
-
import
|
|
3137
|
+
import path11 from "path";
|
|
3138
|
+
import fs9 from "fs/promises";
|
|
3584
3139
|
import fsSync from "fs";
|
|
3585
3140
|
import { execFile as execFile2 } from "child_process";
|
|
3586
3141
|
import { promisify as promisify2 } from "util";
|
|
@@ -3613,8 +3168,8 @@ function mapSupplement(s) {
|
|
|
3613
3168
|
}
|
|
3614
3169
|
|
|
3615
3170
|
// src/utils/MergeRequestHelper.ts
|
|
3616
|
-
import
|
|
3617
|
-
import
|
|
3171
|
+
import fs6 from "fs";
|
|
3172
|
+
import path8 from "path";
|
|
3618
3173
|
var TAPD_PATTERNS = [
|
|
3619
3174
|
/--story=(\d+)/i,
|
|
3620
3175
|
/--bug=(\d+)/i,
|
|
@@ -3658,9 +3213,9 @@ function generateMRDescription(options) {
|
|
|
3658
3213
|
];
|
|
3659
3214
|
const planSections = [];
|
|
3660
3215
|
for (const { filename, label } of summaryFiles) {
|
|
3661
|
-
const filePath =
|
|
3662
|
-
if (
|
|
3663
|
-
const content =
|
|
3216
|
+
const filePath = path8.join(planDir, ".claude-plan", `issue-${issueIid}`, filename);
|
|
3217
|
+
if (fs6.existsSync(filePath)) {
|
|
3218
|
+
const content = fs6.readFileSync(filePath, "utf-8");
|
|
3664
3219
|
const summary = extractSummary(content);
|
|
3665
3220
|
if (summary) {
|
|
3666
3221
|
planSections.push(`### ${label}
|
|
@@ -3684,7 +3239,7 @@ function extractSummary(content, maxLines = 20) {
|
|
|
3684
3239
|
|
|
3685
3240
|
// src/deploy/PortAllocator.ts
|
|
3686
3241
|
import net from "net";
|
|
3687
|
-
var
|
|
3242
|
+
var logger10 = logger.child("PortAllocator");
|
|
3688
3243
|
var DEFAULT_OPTIONS = {
|
|
3689
3244
|
backendPortBase: 4e3,
|
|
3690
3245
|
frontendPortBase: 9e3,
|
|
@@ -3709,7 +3264,7 @@ var PortAllocator = class {
|
|
|
3709
3264
|
async allocate(issueIid) {
|
|
3710
3265
|
const existing = this.allocated.get(issueIid);
|
|
3711
3266
|
if (existing) {
|
|
3712
|
-
|
|
3267
|
+
logger10.info("Returning already allocated ports", { issueIid, ports: existing });
|
|
3713
3268
|
return existing;
|
|
3714
3269
|
}
|
|
3715
3270
|
const usedBackend = new Set([...this.allocated.values()].map((p) => p.backendPort));
|
|
@@ -3727,10 +3282,10 @@ var PortAllocator = class {
|
|
|
3727
3282
|
if (beOk && feOk) {
|
|
3728
3283
|
const pair = { backendPort, frontendPort };
|
|
3729
3284
|
this.allocated.set(issueIid, pair);
|
|
3730
|
-
|
|
3285
|
+
logger10.info("Ports allocated", { issueIid, ...pair });
|
|
3731
3286
|
return pair;
|
|
3732
3287
|
}
|
|
3733
|
-
|
|
3288
|
+
logger10.debug("Port pair unavailable, trying next", {
|
|
3734
3289
|
backendPort,
|
|
3735
3290
|
frontendPort,
|
|
3736
3291
|
beOk,
|
|
@@ -3745,7 +3300,7 @@ var PortAllocator = class {
|
|
|
3745
3300
|
const pair = this.allocated.get(issueIid);
|
|
3746
3301
|
if (pair) {
|
|
3747
3302
|
this.allocated.delete(issueIid);
|
|
3748
|
-
|
|
3303
|
+
logger10.info("Ports released", { issueIid, ...pair });
|
|
3749
3304
|
}
|
|
3750
3305
|
}
|
|
3751
3306
|
getPortsForIssue(issueIid) {
|
|
@@ -3756,15 +3311,15 @@ var PortAllocator = class {
|
|
|
3756
3311
|
}
|
|
3757
3312
|
restore(issueIid, ports) {
|
|
3758
3313
|
this.allocated.set(issueIid, ports);
|
|
3759
|
-
|
|
3314
|
+
logger10.info("Ports restored from persistence", { issueIid, ...ports });
|
|
3760
3315
|
}
|
|
3761
3316
|
};
|
|
3762
3317
|
|
|
3763
3318
|
// src/deploy/DevServerManager.ts
|
|
3764
3319
|
import { spawn } from "child_process";
|
|
3765
|
-
import
|
|
3766
|
-
import
|
|
3767
|
-
var
|
|
3320
|
+
import fs7 from "fs";
|
|
3321
|
+
import path9 from "path";
|
|
3322
|
+
var logger11 = logger.child("DevServerManager");
|
|
3768
3323
|
var DEFAULT_OPTIONS2 = {};
|
|
3769
3324
|
var DevServerManager = class {
|
|
3770
3325
|
servers = /* @__PURE__ */ new Map();
|
|
@@ -3772,25 +3327,25 @@ var DevServerManager = class {
|
|
|
3772
3327
|
logDir;
|
|
3773
3328
|
constructor(options) {
|
|
3774
3329
|
this.options = { ...DEFAULT_OPTIONS2, ...options };
|
|
3775
|
-
this.logDir =
|
|
3776
|
-
if (!
|
|
3777
|
-
|
|
3330
|
+
this.logDir = path9.join(resolveDataDir(), "preview-logs");
|
|
3331
|
+
if (!fs7.existsSync(this.logDir)) {
|
|
3332
|
+
fs7.mkdirSync(this.logDir, { recursive: true });
|
|
3778
3333
|
}
|
|
3779
3334
|
}
|
|
3780
3335
|
getLogPath(issueIid, type) {
|
|
3781
|
-
const filePath =
|
|
3782
|
-
return
|
|
3336
|
+
const filePath = path9.join(this.logDir, `${issueIid}-${type}.log`);
|
|
3337
|
+
return fs7.existsSync(filePath) ? filePath : null;
|
|
3783
3338
|
}
|
|
3784
3339
|
async startServers(wtCtx, ports) {
|
|
3785
3340
|
if (this.servers.has(wtCtx.issueIid)) {
|
|
3786
|
-
|
|
3341
|
+
logger11.info("Servers already running for issue", { issueIid: wtCtx.issueIid });
|
|
3787
3342
|
return;
|
|
3788
3343
|
}
|
|
3789
|
-
|
|
3790
|
-
const backendLogPath =
|
|
3791
|
-
const frontendLogPath =
|
|
3792
|
-
const backendLog =
|
|
3793
|
-
const frontendLog =
|
|
3344
|
+
logger11.info("Starting dev servers", { issueIid: wtCtx.issueIid, ...ports });
|
|
3345
|
+
const backendLogPath = path9.join(this.logDir, `${wtCtx.issueIid}-backend.log`);
|
|
3346
|
+
const frontendLogPath = path9.join(this.logDir, `${wtCtx.issueIid}-frontend.log`);
|
|
3347
|
+
const backendLog = fs7.createWriteStream(backendLogPath, { flags: "a" });
|
|
3348
|
+
const frontendLog = fs7.createWriteStream(frontendLogPath, { flags: "a" });
|
|
3794
3349
|
const tsLine = (stream, data) => `[${(/* @__PURE__ */ new Date()).toISOString()}] [${stream}] ${data.toString().trimEnd()}
|
|
3795
3350
|
`;
|
|
3796
3351
|
const backendEnv = {
|
|
@@ -3814,9 +3369,9 @@ var DevServerManager = class {
|
|
|
3814
3369
|
backendLog.write(tsLine("stderr", data));
|
|
3815
3370
|
});
|
|
3816
3371
|
backend.on("exit", (code) => {
|
|
3817
|
-
|
|
3372
|
+
logger11.info("Backend process exited", { issueIid: wtCtx.issueIid, code });
|
|
3818
3373
|
});
|
|
3819
|
-
const frontendDir =
|
|
3374
|
+
const frontendDir = path9.join(wtCtx.workDir, "frontend");
|
|
3820
3375
|
const frontendEnv = {
|
|
3821
3376
|
...process.env,
|
|
3822
3377
|
BACKEND_PORT: String(ports.backendPort),
|
|
@@ -3838,7 +3393,7 @@ var DevServerManager = class {
|
|
|
3838
3393
|
frontendLog.write(tsLine("stderr", data));
|
|
3839
3394
|
});
|
|
3840
3395
|
frontend.on("exit", (code) => {
|
|
3841
|
-
|
|
3396
|
+
logger11.info("Frontend process exited", { issueIid: wtCtx.issueIid, code });
|
|
3842
3397
|
});
|
|
3843
3398
|
const serverSet = {
|
|
3844
3399
|
backend,
|
|
@@ -3850,14 +3405,14 @@ var DevServerManager = class {
|
|
|
3850
3405
|
frontendLog
|
|
3851
3406
|
};
|
|
3852
3407
|
this.servers.set(wtCtx.issueIid, serverSet);
|
|
3853
|
-
|
|
3408
|
+
logger11.info("Dev servers spawned, waiting for startup", { issueIid: wtCtx.issueIid, ...ports });
|
|
3854
3409
|
await new Promise((r) => setTimeout(r, 1e4));
|
|
3855
|
-
|
|
3410
|
+
logger11.info("Dev servers startup grace period done", { issueIid: wtCtx.issueIid });
|
|
3856
3411
|
}
|
|
3857
3412
|
stopServers(issueIid) {
|
|
3858
3413
|
const set = this.servers.get(issueIid);
|
|
3859
3414
|
if (!set) return;
|
|
3860
|
-
|
|
3415
|
+
logger11.info("Stopping dev servers", { issueIid, ports: set.ports });
|
|
3861
3416
|
killProcess(set.backend, `backend #${issueIid}`);
|
|
3862
3417
|
killProcess(set.frontend, `frontend #${issueIid}`);
|
|
3863
3418
|
set.backendLog.end();
|
|
@@ -3894,7 +3449,7 @@ function killProcess(proc, label) {
|
|
|
3894
3449
|
}
|
|
3895
3450
|
setTimeout(() => {
|
|
3896
3451
|
if (!proc.killed && proc.exitCode === null) {
|
|
3897
|
-
|
|
3452
|
+
logger11.warn(`Force killing ${label}`);
|
|
3898
3453
|
try {
|
|
3899
3454
|
process.kill(-pid, "SIGKILL");
|
|
3900
3455
|
} catch {
|
|
@@ -3903,7 +3458,7 @@ function killProcess(proc, label) {
|
|
|
3903
3458
|
}
|
|
3904
3459
|
}, 5e3);
|
|
3905
3460
|
} catch (err) {
|
|
3906
|
-
|
|
3461
|
+
logger11.warn(`Failed to kill ${label}`, { error: err.message });
|
|
3907
3462
|
}
|
|
3908
3463
|
}
|
|
3909
3464
|
|
|
@@ -3922,13 +3477,13 @@ function isE2eEnabledForIssue(issueIid, tracker, cfg) {
|
|
|
3922
3477
|
}
|
|
3923
3478
|
|
|
3924
3479
|
// src/e2e/ScreenshotCollector.ts
|
|
3925
|
-
import
|
|
3926
|
-
import
|
|
3927
|
-
var
|
|
3480
|
+
import fs8 from "fs";
|
|
3481
|
+
import path10 from "path";
|
|
3482
|
+
var logger12 = logger.child("ScreenshotCollector");
|
|
3928
3483
|
var MAX_SCREENSHOTS = 20;
|
|
3929
3484
|
function walkDir(dir, files = []) {
|
|
3930
|
-
for (const entry of
|
|
3931
|
-
const full =
|
|
3485
|
+
for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
|
|
3486
|
+
const full = path10.join(dir, entry.name);
|
|
3932
3487
|
if (entry.isDirectory()) {
|
|
3933
3488
|
walkDir(full, files);
|
|
3934
3489
|
} else if (entry.isFile() && entry.name.endsWith(".png")) {
|
|
@@ -3938,34 +3493,34 @@ function walkDir(dir, files = []) {
|
|
|
3938
3493
|
return files;
|
|
3939
3494
|
}
|
|
3940
3495
|
function collectScreenshots(workDir) {
|
|
3941
|
-
const testResultsDir =
|
|
3942
|
-
if (!
|
|
3943
|
-
|
|
3496
|
+
const testResultsDir = path10.join(workDir, "frontend", "test-results");
|
|
3497
|
+
if (!fs8.existsSync(testResultsDir)) {
|
|
3498
|
+
logger12.debug("test-results directory not found", { dir: testResultsDir });
|
|
3944
3499
|
return [];
|
|
3945
3500
|
}
|
|
3946
3501
|
const pngFiles = walkDir(testResultsDir);
|
|
3947
3502
|
if (pngFiles.length === 0) {
|
|
3948
|
-
|
|
3503
|
+
logger12.debug("No screenshots found");
|
|
3949
3504
|
return [];
|
|
3950
3505
|
}
|
|
3951
3506
|
const screenshots = pngFiles.map((filePath) => {
|
|
3952
|
-
const relative =
|
|
3953
|
-
const testName = relative.split(
|
|
3507
|
+
const relative = path10.relative(testResultsDir, filePath);
|
|
3508
|
+
const testName = relative.split(path10.sep)[0] || path10.basename(filePath, ".png");
|
|
3954
3509
|
return { filePath, testName };
|
|
3955
3510
|
});
|
|
3956
3511
|
if (screenshots.length > MAX_SCREENSHOTS) {
|
|
3957
|
-
|
|
3512
|
+
logger12.warn("Too many screenshots, truncating", {
|
|
3958
3513
|
total: screenshots.length,
|
|
3959
3514
|
max: MAX_SCREENSHOTS
|
|
3960
3515
|
});
|
|
3961
3516
|
return screenshots.slice(0, MAX_SCREENSHOTS);
|
|
3962
3517
|
}
|
|
3963
|
-
|
|
3518
|
+
logger12.info("Screenshots collected", { count: screenshots.length });
|
|
3964
3519
|
return screenshots;
|
|
3965
3520
|
}
|
|
3966
3521
|
|
|
3967
3522
|
// src/e2e/ScreenshotPublisher.ts
|
|
3968
|
-
var
|
|
3523
|
+
var logger13 = logger.child("ScreenshotPublisher");
|
|
3969
3524
|
function buildComment(uploaded, truncated) {
|
|
3970
3525
|
const lines = [t("screenshot.title"), ""];
|
|
3971
3526
|
for (const item of uploaded) {
|
|
@@ -3984,12 +3539,12 @@ var ScreenshotPublisher = class {
|
|
|
3984
3539
|
const { workDir, issueIid, issueId, mrIid } = options;
|
|
3985
3540
|
const screenshots = collectScreenshots(workDir);
|
|
3986
3541
|
if (screenshots.length === 0) {
|
|
3987
|
-
|
|
3542
|
+
logger13.info("No E2E screenshots to publish", { issueIid });
|
|
3988
3543
|
return;
|
|
3989
3544
|
}
|
|
3990
3545
|
const uploaded = await this.uploadAll(screenshots);
|
|
3991
3546
|
if (uploaded.length === 0) {
|
|
3992
|
-
|
|
3547
|
+
logger13.warn("All screenshot uploads failed", { issueIid });
|
|
3993
3548
|
return;
|
|
3994
3549
|
}
|
|
3995
3550
|
const truncated = screenshots.length >= 20;
|
|
@@ -3998,7 +3553,7 @@ var ScreenshotPublisher = class {
|
|
|
3998
3553
|
if (mrIid) {
|
|
3999
3554
|
await this.postToMergeRequest(mrIid, comment);
|
|
4000
3555
|
}
|
|
4001
|
-
|
|
3556
|
+
logger13.info("E2E screenshots published", {
|
|
4002
3557
|
issueIid,
|
|
4003
3558
|
mrIid,
|
|
4004
3559
|
count: uploaded.length
|
|
@@ -4014,7 +3569,7 @@ var ScreenshotPublisher = class {
|
|
|
4014
3569
|
markdown: result.markdown
|
|
4015
3570
|
});
|
|
4016
3571
|
} catch (err) {
|
|
4017
|
-
|
|
3572
|
+
logger13.warn("Failed to upload screenshot", {
|
|
4018
3573
|
filePath: screenshot.filePath,
|
|
4019
3574
|
error: err.message
|
|
4020
3575
|
});
|
|
@@ -4026,7 +3581,7 @@ var ScreenshotPublisher = class {
|
|
|
4026
3581
|
try {
|
|
4027
3582
|
await this.gongfeng.createIssueNote(issueId, comment);
|
|
4028
3583
|
} catch (err) {
|
|
4029
|
-
|
|
3584
|
+
logger13.warn("Failed to post screenshots to issue", {
|
|
4030
3585
|
issueId,
|
|
4031
3586
|
error: err.message
|
|
4032
3587
|
});
|
|
@@ -4036,7 +3591,7 @@ var ScreenshotPublisher = class {
|
|
|
4036
3591
|
try {
|
|
4037
3592
|
await this.gongfeng.createMergeRequestNote(mrIid, comment);
|
|
4038
3593
|
} catch (err) {
|
|
4039
|
-
|
|
3594
|
+
logger13.warn("Failed to post screenshots to merge request", {
|
|
4040
3595
|
mrIid,
|
|
4041
3596
|
error: err.message
|
|
4042
3597
|
});
|
|
@@ -4219,7 +3774,7 @@ metrics.registerCounter("iaf_braindump_batches_total", "Total braindump batches"
|
|
|
4219
3774
|
metrics.registerCounter("iaf_braindump_tasks_total", "Total braindump tasks");
|
|
4220
3775
|
|
|
4221
3776
|
// src/orchestrator/steps/SetupStep.ts
|
|
4222
|
-
var
|
|
3777
|
+
var logger14 = logger.child("SetupStep");
|
|
4223
3778
|
async function executeSetup(ctx, deps) {
|
|
4224
3779
|
const { issue, wtCtx, record, pipelineDef, branchName } = ctx;
|
|
4225
3780
|
try {
|
|
@@ -4228,7 +3783,7 @@ async function executeSetup(ctx, deps) {
|
|
|
4228
3783
|
"auto-finish:processing"
|
|
4229
3784
|
]);
|
|
4230
3785
|
} catch (err) {
|
|
4231
|
-
|
|
3786
|
+
logger14.warn("Failed to update issue labels", { error: err.message });
|
|
4232
3787
|
}
|
|
4233
3788
|
await deps.mainGitMutex.runExclusive(async () => {
|
|
4234
3789
|
deps.emitProgress(issue.iid, "fetch", t("orchestrator.fetchProgress"));
|
|
@@ -4287,7 +3842,7 @@ async function executeSetup(ctx, deps) {
|
|
|
4287
3842
|
issueDescription: issue.description
|
|
4288
3843
|
});
|
|
4289
3844
|
} catch (err) {
|
|
4290
|
-
|
|
3845
|
+
logger14.warn("Failed to inject Claude Code hooks (non-blocking)", {
|
|
4291
3846
|
error: err.message
|
|
4292
3847
|
});
|
|
4293
3848
|
}
|
|
@@ -4304,6 +3859,44 @@ async function executeSetup(ctx, deps) {
|
|
|
4304
3859
|
return { wtGit, wtPlan, wtGitMap };
|
|
4305
3860
|
}
|
|
4306
3861
|
|
|
3862
|
+
// src/lifecycle/FeedbackTypes.ts
|
|
3863
|
+
function inputRequestToFeedback(request) {
|
|
3864
|
+
switch (request.type) {
|
|
3865
|
+
case "interactive-dialog":
|
|
3866
|
+
return {
|
|
3867
|
+
kind: "interactive-dialog",
|
|
3868
|
+
question: request.content,
|
|
3869
|
+
options: request.options ?? []
|
|
3870
|
+
};
|
|
3871
|
+
case "plan-approval":
|
|
3872
|
+
return { kind: "approval-required", scope: "plan" };
|
|
3873
|
+
case "generic":
|
|
3874
|
+
return { kind: "generic", content: request.content };
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
function feedbackResponseToString(response) {
|
|
3878
|
+
switch (response.action) {
|
|
3879
|
+
case "approve":
|
|
3880
|
+
return "allow";
|
|
3881
|
+
case "reject":
|
|
3882
|
+
return "reject";
|
|
3883
|
+
case "select":
|
|
3884
|
+
return response.value;
|
|
3885
|
+
case "dismiss":
|
|
3886
|
+
return "";
|
|
3887
|
+
case "pause":
|
|
3888
|
+
return "";
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
function stringToFeedbackResponse(str, originalFeedback) {
|
|
3892
|
+
if (str === "allow") return { action: "approve" };
|
|
3893
|
+
if (str === "reject") return { action: "reject" };
|
|
3894
|
+
if (str === "") {
|
|
3895
|
+
return originalFeedback.kind === "interactive-dialog" ? { action: "dismiss" } : { action: "approve" };
|
|
3896
|
+
}
|
|
3897
|
+
return { action: "select", value: str };
|
|
3898
|
+
}
|
|
3899
|
+
|
|
4307
3900
|
// src/notesync/NoteSyncSettings.ts
|
|
4308
3901
|
var noteSyncOverride;
|
|
4309
3902
|
function getNoteSyncEnabled(cfg) {
|
|
@@ -4360,8 +3953,14 @@ function clearPendingDialog(issueIid) {
|
|
|
4360
3953
|
store.delete(issueIid);
|
|
4361
3954
|
}
|
|
4362
3955
|
|
|
4363
|
-
// src/orchestrator/steps/
|
|
4364
|
-
var
|
|
3956
|
+
// src/orchestrator/steps/PhaseHelpers.ts
|
|
3957
|
+
var logger15 = logger.child("PhaseHelpers");
|
|
3958
|
+
async function safeComment(deps, issueId, message) {
|
|
3959
|
+
try {
|
|
3960
|
+
await deps.gongfeng.createIssueNote(issueId, message);
|
|
3961
|
+
} catch {
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
4365
3964
|
function resolveVerifyRunner(deps) {
|
|
4366
3965
|
return deps.aiRunner;
|
|
4367
3966
|
}
|
|
@@ -4377,7 +3976,7 @@ async function commitPlanFiles(ctx, wtGit, wtGitMap, phaseName, displayId) {
|
|
|
4377
3976
|
for (const repo of ctx.workspace.repos) {
|
|
4378
3977
|
const repoGit = wtGitMap?.get(repo.name);
|
|
4379
3978
|
if (!repoGit) {
|
|
4380
|
-
|
|
3979
|
+
logger15.warn("Missing GitOperations for repo, skipping commit", { repo: repo.name });
|
|
4381
3980
|
continue;
|
|
4382
3981
|
}
|
|
4383
3982
|
const branch = repo.branchPrefix ? `${repo.branchPrefix}-${displayId}` : ctx.branchName;
|
|
@@ -4386,10 +3985,10 @@ async function commitPlanFiles(ctx, wtGit, wtGitMap, phaseName, displayId) {
|
|
|
4386
3985
|
await repoGit.add(["."]);
|
|
4387
3986
|
await repoGit.commit(commitMsg);
|
|
4388
3987
|
await repoGit.push(branch);
|
|
4389
|
-
|
|
3988
|
+
logger15.info("Committed changes for repo", { repo: repo.name, branch });
|
|
4390
3989
|
}
|
|
4391
3990
|
} catch (err) {
|
|
4392
|
-
|
|
3991
|
+
logger15.warn("Failed to commit/push for repo", {
|
|
4393
3992
|
repo: repo.name,
|
|
4394
3993
|
error: err.message
|
|
4395
3994
|
});
|
|
@@ -4427,27 +4026,15 @@ async function syncResultToIssue(phase, ctx, displayId, phaseName, deps, issueId
|
|
|
4427
4026
|
summary
|
|
4428
4027
|
);
|
|
4429
4028
|
await safeComment(deps, issueId, comment);
|
|
4430
|
-
|
|
4029
|
+
logger15.info("Result synced to issue", { issueIid: displayId, file: file.filename });
|
|
4431
4030
|
}
|
|
4432
4031
|
} catch (err) {
|
|
4433
|
-
|
|
4032
|
+
logger15.warn("Failed to sync result to issue", { error: err.message });
|
|
4434
4033
|
await safeComment(deps, issueId, issueProgressComment(phaseName, "completed"));
|
|
4435
4034
|
}
|
|
4436
4035
|
}
|
|
4437
|
-
function buildInputHandler(displayId, phaseName, deps) {
|
|
4438
|
-
const useAcpGate = deps.config.ai.codebuddyAcpAutoApprove === false;
|
|
4439
|
-
return (request) => {
|
|
4440
|
-
if (request.type === "interactive-dialog") {
|
|
4441
|
-
return handleInteractiveDialog(displayId, phaseName, deps, request);
|
|
4442
|
-
}
|
|
4443
|
-
if (request.type === "plan-approval" && useAcpGate) {
|
|
4444
|
-
return handlePlanApproval(displayId, phaseName, deps);
|
|
4445
|
-
}
|
|
4446
|
-
return Promise.resolve("allow");
|
|
4447
|
-
};
|
|
4448
|
-
}
|
|
4449
4036
|
function handlePlanApproval(displayId, phaseName, deps) {
|
|
4450
|
-
|
|
4037
|
+
logger15.info("ACP plan-approval requested, delegating to review gate", {
|
|
4451
4038
|
issueIid: displayId,
|
|
4452
4039
|
phase: phaseName
|
|
4453
4040
|
});
|
|
@@ -4457,14 +4044,14 @@ function handlePlanApproval(displayId, phaseName, deps) {
|
|
|
4457
4044
|
const data = payload.data;
|
|
4458
4045
|
if (data.issueIid !== displayId) return;
|
|
4459
4046
|
cleanup();
|
|
4460
|
-
|
|
4047
|
+
logger15.info("ACP plan-approval approved via review gate", { issueIid: displayId });
|
|
4461
4048
|
resolve("allow");
|
|
4462
4049
|
};
|
|
4463
4050
|
const onRejected = (payload) => {
|
|
4464
4051
|
const data = payload.data;
|
|
4465
4052
|
if (data.issueIid !== displayId) return;
|
|
4466
4053
|
cleanup();
|
|
4467
|
-
|
|
4054
|
+
logger15.info("ACP plan-approval rejected via review gate", { issueIid: displayId });
|
|
4468
4055
|
resolve("reject");
|
|
4469
4056
|
};
|
|
4470
4057
|
const cleanup = () => {
|
|
@@ -4476,7 +4063,7 @@ function handlePlanApproval(displayId, phaseName, deps) {
|
|
|
4476
4063
|
});
|
|
4477
4064
|
}
|
|
4478
4065
|
function handleInteractiveDialog(displayId, phaseName, deps, request) {
|
|
4479
|
-
|
|
4066
|
+
logger15.info("Interactive dialog forwarded to frontend", {
|
|
4480
4067
|
issueIid: displayId,
|
|
4481
4068
|
phase: phaseName,
|
|
4482
4069
|
question: request.content.slice(0, 80),
|
|
@@ -4506,7 +4093,7 @@ function handleInteractiveDialog(displayId, phaseName, deps, request) {
|
|
|
4506
4093
|
if (data.issueIid !== displayId) return;
|
|
4507
4094
|
cleanup();
|
|
4508
4095
|
clearPendingDialog(displayId);
|
|
4509
|
-
|
|
4096
|
+
logger15.info("Interactive dialog response received from frontend", {
|
|
4510
4097
|
issueIid: displayId,
|
|
4511
4098
|
response: data.response
|
|
4512
4099
|
});
|
|
@@ -4516,7 +4103,7 @@ function handleInteractiveDialog(displayId, phaseName, deps, request) {
|
|
|
4516
4103
|
const data = payload.data;
|
|
4517
4104
|
if (data.issueIid !== displayId) return;
|
|
4518
4105
|
cleanup();
|
|
4519
|
-
|
|
4106
|
+
logger15.info("Interactive dialog dismissed by user (false positive)", { issueIid: displayId });
|
|
4520
4107
|
resolve("");
|
|
4521
4108
|
};
|
|
4522
4109
|
deps.eventBus.on("agent:inputResponse", onResponse);
|
|
@@ -4540,45 +4127,75 @@ function updateHooksForPhase(spec, pipelineDef, ctx, wtPlan) {
|
|
|
4540
4127
|
issueDescription: ctx.issue.description
|
|
4541
4128
|
});
|
|
4542
4129
|
} catch (err) {
|
|
4543
|
-
|
|
4130
|
+
logger15.warn("Failed to update hooks for phase (non-blocking)", {
|
|
4544
4131
|
phase: spec.name,
|
|
4545
4132
|
error: err.message
|
|
4546
4133
|
});
|
|
4547
4134
|
}
|
|
4548
4135
|
}
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
}
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
deps.eventBus.emitTyped("agent:output", {
|
|
4567
|
-
issueIid: displayId,
|
|
4568
|
-
phase: spec.name,
|
|
4569
|
-
event: { type: "system", content: t("basePhase.aiStarting", { label: phaseLabel }), timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
4570
|
-
});
|
|
4571
|
-
const callbacks = {
|
|
4572
|
-
onStreamEvent: (event) => deps.eventBus.emitTyped("agent:output", {
|
|
4136
|
+
|
|
4137
|
+
// src/lifecycle/DefaultLifecycleHook.ts
|
|
4138
|
+
var logger16 = logger.child("DefaultLifecycleHook");
|
|
4139
|
+
var DefaultLifecycleHook = class {
|
|
4140
|
+
async beforePhase(ctx) {
|
|
4141
|
+
const { spec, issueCtx, deps, wtPlan } = ctx;
|
|
4142
|
+
const { issue } = issueCtx;
|
|
4143
|
+
const displayId = issue.iid;
|
|
4144
|
+
deps.tracker.updateState(displayId, spec.startState, { currentPhase: spec.name });
|
|
4145
|
+
deps.tracker.updatePhaseProgress(displayId, spec.name, {
|
|
4146
|
+
status: "in_progress",
|
|
4147
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4148
|
+
});
|
|
4149
|
+
wtPlan.updatePhaseProgress(spec.name, "in_progress");
|
|
4150
|
+
await safeComment(deps, issue.id, issueProgressComment(spec.name, "in_progress"));
|
|
4151
|
+
const phaseLabel = t(`phase.${spec.name}`) || spec.name;
|
|
4152
|
+
deps.eventBus.emitTyped("agent:output", {
|
|
4573
4153
|
issueIid: displayId,
|
|
4574
4154
|
phase: spec.name,
|
|
4155
|
+
event: {
|
|
4156
|
+
type: "system",
|
|
4157
|
+
content: t("basePhase.aiStarting", { label: phaseLabel }),
|
|
4158
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4159
|
+
}
|
|
4160
|
+
});
|
|
4161
|
+
}
|
|
4162
|
+
onStream(ctx, event) {
|
|
4163
|
+
const { spec, issueCtx, deps } = ctx;
|
|
4164
|
+
deps.eventBus.emitTyped("agent:output", {
|
|
4165
|
+
issueIid: issueCtx.issue.iid,
|
|
4166
|
+
phase: spec.name,
|
|
4575
4167
|
event
|
|
4576
|
-
})
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4168
|
+
});
|
|
4169
|
+
}
|
|
4170
|
+
beforeExecute;
|
|
4171
|
+
afterExecute;
|
|
4172
|
+
async onFeedback(ctx, feedback) {
|
|
4173
|
+
const { deps, displayId, spec } = ctx;
|
|
4174
|
+
if (feedback.kind === "interactive-dialog") {
|
|
4175
|
+
const result = await handleInteractiveDialog(displayId, spec.name, deps, {
|
|
4176
|
+
type: "interactive-dialog",
|
|
4177
|
+
content: feedback.question,
|
|
4178
|
+
options: feedback.options
|
|
4179
|
+
});
|
|
4180
|
+
return stringToFeedbackResponse(result, feedback);
|
|
4181
|
+
}
|
|
4182
|
+
if (feedback.kind === "approval-required" && feedback.scope === "plan") {
|
|
4183
|
+
const useAcpGate = deps.config.ai.codebuddyAcpAutoApprove === false;
|
|
4184
|
+
if (useAcpGate) {
|
|
4185
|
+
const result = await handlePlanApproval(displayId, spec.name, deps);
|
|
4186
|
+
return result === "allow" ? { action: "approve" } : { action: "reject" };
|
|
4187
|
+
}
|
|
4188
|
+
return { action: "approve" };
|
|
4189
|
+
}
|
|
4190
|
+
return { action: "approve" };
|
|
4191
|
+
}
|
|
4192
|
+
async afterPhase(ctx, outcome) {
|
|
4193
|
+
const { spec, issueCtx, deps, wtGit, wtPlan, wtGitMap, phase } = ctx;
|
|
4194
|
+
const { issue, phaseCtx } = issueCtx;
|
|
4195
|
+
const displayId = issue.iid;
|
|
4196
|
+
if (outcome.sessionId) {
|
|
4197
|
+
wtPlan.updatePhaseSessionId(spec.name, outcome.sessionId);
|
|
4198
|
+
}
|
|
4582
4199
|
deps.tracker.updateState(displayId, spec.doneState, { currentPhase: spec.name });
|
|
4583
4200
|
deps.tracker.updatePhaseProgress(displayId, spec.name, {
|
|
4584
4201
|
status: "completed",
|
|
@@ -4586,28 +4203,400 @@ async function runPhaseWithLifecycle(phase, phaseCtx, spec, ctx, deps, wtGit, wt
|
|
|
4586
4203
|
});
|
|
4587
4204
|
wtPlan.updatePhaseProgress(spec.name, "completed");
|
|
4588
4205
|
await commitPlanFiles(phaseCtx, wtGit, wtGitMap, spec.name, displayId);
|
|
4589
|
-
|
|
4590
|
-
|
|
4206
|
+
if (phase) {
|
|
4207
|
+
await syncResultToIssue(phase, phaseCtx, displayId, spec.name, deps, issue.id, wtPlan);
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
async onError(ctx, error) {
|
|
4211
|
+
const { spec, issueCtx, deps, wtPlan } = ctx;
|
|
4212
|
+
const { issue } = issueCtx;
|
|
4213
|
+
const displayId = issue.iid;
|
|
4214
|
+
wtPlan.updatePhaseProgress(spec.name, "failed", error.message);
|
|
4215
|
+
deps.tracker.updatePhaseProgress(displayId, spec.name, { status: "failed" });
|
|
4216
|
+
if (error.wasActiveAtTimeout) {
|
|
4217
|
+
deps.tracker.markFailedSoft(displayId, error.message, "phase_running" /* PhaseRunning */);
|
|
4218
|
+
} else {
|
|
4219
|
+
deps.tracker.markFailed(displayId, error.message, "phase_running" /* PhaseRunning */);
|
|
4220
|
+
}
|
|
4221
|
+
const shortErr = error.message.slice(0, 200);
|
|
4222
|
+
await safeComment(deps, issue.id, issueProgressComment(spec.name, "failed", shortErr));
|
|
4223
|
+
return { action: "fail", softFail: error.wasActiveAtTimeout };
|
|
4591
4224
|
}
|
|
4592
|
-
|
|
4593
|
-
|
|
4225
|
+
};
|
|
4226
|
+
function createCallbacksFromHook(hook, ctx) {
|
|
4227
|
+
return {
|
|
4228
|
+
onStreamEvent: hook.onStream ? (event) => hook.onStream(ctx, event) : void 0,
|
|
4229
|
+
onInputRequired: hook.onFeedback ? async (request) => {
|
|
4230
|
+
const feedback = inputRequestToFeedback(request);
|
|
4231
|
+
const response = await hook.onFeedback(ctx, feedback);
|
|
4232
|
+
return feedbackResponseToString(response);
|
|
4233
|
+
} : void 0
|
|
4234
|
+
};
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
// src/orchestrator/strategies/GateStrategy.ts
|
|
4238
|
+
var logger17 = logger.child("GateStrategy");
|
|
4239
|
+
var GateStrategy = class {
|
|
4240
|
+
name = "gate";
|
|
4241
|
+
shouldSkip() {
|
|
4242
|
+
return false;
|
|
4594
4243
|
}
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4244
|
+
async execute(ctx, _hooks) {
|
|
4245
|
+
const { spec, issueCtx, deps, wtPlan } = ctx;
|
|
4246
|
+
const { issue } = issueCtx;
|
|
4247
|
+
if (deps.shouldAutoApprove(issue.labels)) {
|
|
4248
|
+
logger17.info("Auto-approving review gate (matched autoApproveLabels)", {
|
|
4249
|
+
iid: issue.iid,
|
|
4250
|
+
labels: issue.labels,
|
|
4251
|
+
autoApproveLabels: deps.config.review.autoApproveLabels
|
|
4252
|
+
});
|
|
4253
|
+
if (spec.approvedState) {
|
|
4254
|
+
deps.tracker.updateState(issue.iid, spec.approvedState, { currentPhase: spec.name });
|
|
4255
|
+
}
|
|
4256
|
+
wtPlan.updatePhaseProgress(spec.name, "completed");
|
|
4257
|
+
deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
|
|
4258
|
+
status: "completed",
|
|
4259
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4260
|
+
});
|
|
4261
|
+
try {
|
|
4262
|
+
await deps.gongfeng.createIssueNote(issue.id, t("orchestrator.autoApproveComment"));
|
|
4263
|
+
} catch {
|
|
4264
|
+
}
|
|
4265
|
+
return { paused: false };
|
|
4266
|
+
}
|
|
4267
|
+
deps.tracker.updateState(issue.iid, spec.startState, { currentPhase: spec.name });
|
|
4268
|
+
wtPlan.updatePhaseProgress(spec.name, "in_progress");
|
|
4269
|
+
deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
|
|
4270
|
+
status: "in_progress",
|
|
4271
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4272
|
+
});
|
|
4273
|
+
deps.eventBus.emitTyped("review:requested", { issueIid: issue.iid });
|
|
4274
|
+
logger17.info("Review gate reached, pausing", { iid: issue.iid });
|
|
4275
|
+
return { paused: true };
|
|
4604
4276
|
}
|
|
4605
|
-
|
|
4277
|
+
};
|
|
4278
|
+
|
|
4279
|
+
// src/orchestrator/strategies/AiPhaseStrategy.ts
|
|
4280
|
+
var logger18 = logger.child("AiPhaseStrategy");
|
|
4281
|
+
var AiPhaseStrategy = class {
|
|
4282
|
+
name = "ai";
|
|
4283
|
+
shouldSkip(ctx) {
|
|
4284
|
+
const { spec, issueCtx, deps } = ctx;
|
|
4285
|
+
if (spec.name === "uat" && !isE2eEnabledForIssue(issueCtx.issue.iid, deps.tracker, deps.config)) {
|
|
4286
|
+
logger18.info("UAT phase skipped (E2E not enabled for this issue)", { iid: issueCtx.issue.iid });
|
|
4287
|
+
return true;
|
|
4288
|
+
}
|
|
4289
|
+
return false;
|
|
4290
|
+
}
|
|
4291
|
+
async execute(ctx, hooks) {
|
|
4292
|
+
const { spec, issueCtx, deps, wtGit, wtPlan, wtGitMap } = ctx;
|
|
4293
|
+
const { issue, phaseCtx } = issueCtx;
|
|
4294
|
+
if (this.shouldSkip(ctx)) {
|
|
4295
|
+
deps.tracker.updateState(issue.iid, spec.doneState, { currentPhase: spec.name });
|
|
4296
|
+
wtPlan.updatePhaseProgress(spec.name, "completed");
|
|
4297
|
+
deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
|
|
4298
|
+
status: "completed",
|
|
4299
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4300
|
+
});
|
|
4301
|
+
return { paused: false };
|
|
4302
|
+
}
|
|
4303
|
+
updateHooksForPhase(spec, issueCtx.pipelineDef, issueCtx, wtPlan);
|
|
4304
|
+
const runner = this.resolveRunner(ctx);
|
|
4305
|
+
if (spec.name === "uat") {
|
|
4306
|
+
const runnerName = runner === deps.e2eAiRunner ? "e2eAiRunner (CodeBuddy)" : "mainRunner";
|
|
4307
|
+
logger18.info("UAT phase starting", { iid: issue.iid, runner: runnerName });
|
|
4308
|
+
}
|
|
4309
|
+
const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config);
|
|
4310
|
+
if (wtGitMap) phase.setWtGitMap(wtGitMap);
|
|
4311
|
+
ctx.phase = phase;
|
|
4312
|
+
const outcome = await runPhaseWithLifecycle(
|
|
4313
|
+
phase,
|
|
4314
|
+
phaseCtx,
|
|
4315
|
+
spec,
|
|
4316
|
+
issueCtx,
|
|
4317
|
+
deps,
|
|
4318
|
+
wtGit,
|
|
4319
|
+
wtPlan,
|
|
4320
|
+
wtGitMap,
|
|
4321
|
+
hooks
|
|
4322
|
+
);
|
|
4323
|
+
if (outcome.status === "running") {
|
|
4324
|
+
return this.handleAsyncOutcome(ctx, outcome);
|
|
4325
|
+
}
|
|
4326
|
+
if (spec.approvedState && outcome.data?.hasReleaseCapability) {
|
|
4327
|
+
return this.handleGateRequest(ctx);
|
|
4328
|
+
}
|
|
4329
|
+
return { paused: false };
|
|
4330
|
+
}
|
|
4331
|
+
resolveRunner(ctx) {
|
|
4332
|
+
const { spec, deps, displayId } = ctx;
|
|
4333
|
+
if (spec.name === "verify") return resolveVerifyRunner(deps);
|
|
4334
|
+
if (spec.name === "uat") return resolveUatRunner(deps, displayId);
|
|
4335
|
+
return deps.aiRunner;
|
|
4336
|
+
}
|
|
4337
|
+
async handleAsyncOutcome(ctx, outcome) {
|
|
4338
|
+
const { spec, issueCtx, deps, wtGit, wtPlan, wtGitMap } = ctx;
|
|
4339
|
+
const { issue, phaseCtx } = issueCtx;
|
|
4340
|
+
if (outcome.awaitCompletion) {
|
|
4341
|
+
logger18.info("Async phase running, awaiting completion", { iid: issue.iid, phase: spec.name });
|
|
4342
|
+
const finalOutcome = await outcome.awaitCompletion;
|
|
4343
|
+
if (finalOutcome.sessionId) {
|
|
4344
|
+
wtPlan.updatePhaseSessionId(spec.name, finalOutcome.sessionId);
|
|
4345
|
+
}
|
|
4346
|
+
if (finalOutcome.status === "completed") {
|
|
4347
|
+
deps.tracker.updateState(issue.iid, spec.doneState, { currentPhase: spec.name });
|
|
4348
|
+
deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
|
|
4349
|
+
status: "completed",
|
|
4350
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4351
|
+
});
|
|
4352
|
+
wtPlan.updatePhaseProgress(spec.name, "completed");
|
|
4353
|
+
await commitPlanFiles(phaseCtx, wtGit, wtGitMap, spec.name, issue.iid);
|
|
4354
|
+
const runner = this.resolveRunner(ctx);
|
|
4355
|
+
const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config);
|
|
4356
|
+
await syncResultToIssue(phase, phaseCtx, issue.iid, spec.name, deps, issue.id, wtPlan);
|
|
4357
|
+
logger18.info("Async phase completed successfully", { iid: issue.iid, phase: spec.name });
|
|
4358
|
+
return { paused: false };
|
|
4359
|
+
}
|
|
4360
|
+
const errMsg = finalOutcome.error?.message ?? "Unknown error";
|
|
4361
|
+
const shortErr = errMsg.slice(0, 200);
|
|
4362
|
+
const wasActive = finalOutcome.error?.wasActiveAtTimeout ?? false;
|
|
4363
|
+
wtPlan.updatePhaseProgress(spec.name, "failed", errMsg);
|
|
4364
|
+
deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "failed" });
|
|
4365
|
+
if (wasActive) {
|
|
4366
|
+
deps.tracker.markFailedSoft(issue.iid, errMsg, "phase_running" /* PhaseRunning */);
|
|
4367
|
+
} else {
|
|
4368
|
+
deps.tracker.markFailed(issue.iid, errMsg, "phase_running" /* PhaseRunning */);
|
|
4369
|
+
}
|
|
4370
|
+
await safeComment(deps, issue.id, issueProgressComment(spec.name, "failed", shortErr));
|
|
4371
|
+
throw new AIExecutionError(spec.name, `Phase ${spec.name} failed: ${shortErr}`, {
|
|
4372
|
+
output: finalOutcome.error?.rawOutput ?? finalOutcome.output,
|
|
4373
|
+
exitCode: finalOutcome.exitCode ?? 1,
|
|
4374
|
+
isRetryable: finalOutcome.error?.isRetryable,
|
|
4375
|
+
wasActiveAtTimeout: wasActive
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
deps.tracker.updateState(issue.iid, "phase_waiting" /* PhaseWaiting */, { currentPhase: spec.name });
|
|
4379
|
+
wtPlan.updatePhaseProgress(spec.name, "gate_waiting");
|
|
4380
|
+
deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "gate_waiting" });
|
|
4381
|
+
const gateEvent = spec.name === "uat" ? "uat:gateRequested" : "release:gateRequested";
|
|
4382
|
+
deps.eventBus.emitTyped(gateEvent, { issueIid: issue.iid });
|
|
4383
|
+
logger18.info("Async phase running (no awaitCompletion), pausing pipeline", {
|
|
4384
|
+
iid: issue.iid,
|
|
4385
|
+
phase: spec.name
|
|
4386
|
+
});
|
|
4387
|
+
return { paused: true };
|
|
4388
|
+
}
|
|
4389
|
+
handleGateRequest(ctx) {
|
|
4390
|
+
const { spec, issueCtx, deps, wtPlan } = ctx;
|
|
4391
|
+
const { issue } = issueCtx;
|
|
4392
|
+
deps.tracker.updateState(issue.iid, "phase_waiting" /* PhaseWaiting */, { currentPhase: spec.name });
|
|
4393
|
+
wtPlan.updatePhaseProgress(spec.name, "gate_waiting");
|
|
4394
|
+
deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "gate_waiting" });
|
|
4395
|
+
deps.eventBus.emitTyped("release:gateRequested", { issueIid: issue.iid });
|
|
4396
|
+
logger18.info("Phase requested gate, pausing", { iid: issue.iid, phase: spec.name });
|
|
4397
|
+
return { paused: true };
|
|
4398
|
+
}
|
|
4399
|
+
};
|
|
4400
|
+
|
|
4401
|
+
// src/orchestrator/strategies/VerifyFixStrategy.ts
|
|
4402
|
+
var logger19 = logger.child("VerifyFixStrategy");
|
|
4403
|
+
var VerifyFixStrategy = class {
|
|
4404
|
+
name = "verify-fix";
|
|
4405
|
+
shouldSkip() {
|
|
4406
|
+
return false;
|
|
4407
|
+
}
|
|
4408
|
+
async execute(ctx, hooks) {
|
|
4409
|
+
const { issueCtx, deps, wtGit, wtPlan, wtGitMap } = ctx;
|
|
4410
|
+
const { issue, phaseCtx, pipelineDef } = issueCtx;
|
|
4411
|
+
const maxIterations = deps.config.verifyFixLoop.maxIterations;
|
|
4412
|
+
const verifySpec = ctx.spec;
|
|
4413
|
+
const verifyPhaseIdx = pipelineDef.phases.findIndex((p) => p.name === verifySpec.name);
|
|
4414
|
+
const buildPhaseIdx = this.findPreviousAiPhaseIndex(pipelineDef.phases, verifyPhaseIdx);
|
|
4415
|
+
deps.eventBus.emitTyped("verify:loopStarted", {
|
|
4416
|
+
issueIid: issue.iid,
|
|
4417
|
+
maxIterations
|
|
4418
|
+
});
|
|
4419
|
+
logger19.info("Verify-fix loop started", {
|
|
4420
|
+
iid: issue.iid,
|
|
4421
|
+
maxIterations,
|
|
4422
|
+
buildPhaseIdx
|
|
4423
|
+
});
|
|
4424
|
+
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
4425
|
+
if (isShuttingDown()) throw new ServiceShutdownError();
|
|
4426
|
+
logger19.info("Verify-fix loop iteration", { iteration, maxIterations, iid: issue.iid });
|
|
4427
|
+
updateHooksForPhase(verifySpec, pipelineDef, issueCtx, wtPlan);
|
|
4428
|
+
const verifyRunner = resolveVerifyRunner(deps);
|
|
4429
|
+
const verifyPhase = createPhase("verify", verifyRunner, wtGit, wtPlan, deps.config);
|
|
4430
|
+
if (wtGitMap) verifyPhase.setWtGitMap(wtGitMap);
|
|
4431
|
+
let verifyOutcome;
|
|
4432
|
+
try {
|
|
4433
|
+
verifyOutcome = await runPhaseWithLifecycle(
|
|
4434
|
+
verifyPhase,
|
|
4435
|
+
phaseCtx,
|
|
4436
|
+
verifySpec,
|
|
4437
|
+
issueCtx,
|
|
4438
|
+
deps,
|
|
4439
|
+
wtGit,
|
|
4440
|
+
wtPlan,
|
|
4441
|
+
wtGitMap,
|
|
4442
|
+
hooks
|
|
4443
|
+
);
|
|
4444
|
+
} catch (err) {
|
|
4445
|
+
logger19.warn("Verify phase execution failed", {
|
|
4446
|
+
iteration,
|
|
4447
|
+
iid: issue.iid,
|
|
4448
|
+
error: err.message
|
|
4449
|
+
});
|
|
4450
|
+
deps.eventBus.emitTyped("verify:iterationComplete", {
|
|
4451
|
+
issueIid: issue.iid,
|
|
4452
|
+
iteration,
|
|
4453
|
+
passed: false,
|
|
4454
|
+
failures: ["AI runner execution failed"]
|
|
4455
|
+
});
|
|
4456
|
+
if (iteration === maxIterations) throw err;
|
|
4457
|
+
if (buildPhaseIdx >= 0) {
|
|
4458
|
+
await this.executeBuildFix(issueCtx, deps, wtGit, wtPlan, buildPhaseIdx, {
|
|
4459
|
+
iteration,
|
|
4460
|
+
verifyFailures: ["AI runner execution failed: " + err.message],
|
|
4461
|
+
rawReport: ""
|
|
4462
|
+
}, wtGitMap, hooks);
|
|
4463
|
+
}
|
|
4464
|
+
continue;
|
|
4465
|
+
}
|
|
4466
|
+
const report = verifyOutcome.data?.verifyReport;
|
|
4467
|
+
const passed = report ? report.passed : true;
|
|
4468
|
+
deps.eventBus.emitTyped("verify:iterationComplete", {
|
|
4469
|
+
issueIid: issue.iid,
|
|
4470
|
+
iteration,
|
|
4471
|
+
passed,
|
|
4472
|
+
failures: report?.failureReasons
|
|
4473
|
+
});
|
|
4474
|
+
if (passed) {
|
|
4475
|
+
logger19.info("Verify-fix loop passed", { iteration, iid: issue.iid });
|
|
4476
|
+
return { paused: false };
|
|
4477
|
+
}
|
|
4478
|
+
logger19.info("Verify failed, issues found", {
|
|
4479
|
+
iteration,
|
|
4480
|
+
iid: issue.iid,
|
|
4481
|
+
failures: report?.failureReasons,
|
|
4482
|
+
todolistStats: report?.todolistStats
|
|
4483
|
+
});
|
|
4484
|
+
if (iteration === maxIterations) {
|
|
4485
|
+
deps.eventBus.emitTyped("verify:loopExhausted", {
|
|
4486
|
+
issueIid: issue.iid,
|
|
4487
|
+
totalIterations: iteration,
|
|
4488
|
+
failures: report?.failureReasons ?? []
|
|
4489
|
+
});
|
|
4490
|
+
const failMsg = `Verify-fix loop exhausted after ${maxIterations} iterations. Remaining issues: ${report?.failureReasons?.join("; ") ?? "unknown"}`;
|
|
4491
|
+
logger19.warn(failMsg, { iid: issue.iid });
|
|
4492
|
+
throw new AIExecutionError("verify", failMsg, {
|
|
4493
|
+
output: report?.rawReport ?? "",
|
|
4494
|
+
exitCode: 0
|
|
4495
|
+
});
|
|
4496
|
+
}
|
|
4497
|
+
if (buildPhaseIdx >= 0) {
|
|
4498
|
+
await this.executeBuildFix(issueCtx, deps, wtGit, wtPlan, buildPhaseIdx, {
|
|
4499
|
+
iteration,
|
|
4500
|
+
verifyFailures: report?.failureReasons ?? [],
|
|
4501
|
+
rawReport: report?.rawReport ?? ""
|
|
4502
|
+
}, wtGitMap, hooks);
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
return { paused: false };
|
|
4506
|
+
}
|
|
4507
|
+
async executeBuildFix(ctx, deps, wtGit, wtPlan, buildPhaseIdx, fixContext, wtGitMap, hooks) {
|
|
4508
|
+
const { issue, phaseCtx, pipelineDef } = ctx;
|
|
4509
|
+
const buildSpec = pipelineDef.phases[buildPhaseIdx];
|
|
4510
|
+
logger19.info("Looping back to build for fix", {
|
|
4511
|
+
iteration: fixContext.iteration,
|
|
4512
|
+
iid: issue.iid,
|
|
4513
|
+
failures: fixContext.verifyFailures
|
|
4514
|
+
});
|
|
4515
|
+
phaseCtx.fixContext = fixContext;
|
|
4516
|
+
try {
|
|
4517
|
+
updateHooksForPhase(buildSpec, pipelineDef, ctx, wtPlan);
|
|
4518
|
+
const buildPhase = createPhase("build", deps.aiRunner, wtGit, wtPlan, deps.config);
|
|
4519
|
+
if (wtGitMap) buildPhase.setWtGitMap(wtGitMap);
|
|
4520
|
+
await runPhaseWithLifecycle(
|
|
4521
|
+
buildPhase,
|
|
4522
|
+
phaseCtx,
|
|
4523
|
+
buildSpec,
|
|
4524
|
+
ctx,
|
|
4525
|
+
deps,
|
|
4526
|
+
wtGit,
|
|
4527
|
+
wtPlan,
|
|
4528
|
+
wtGitMap,
|
|
4529
|
+
hooks
|
|
4530
|
+
);
|
|
4531
|
+
} finally {
|
|
4532
|
+
delete phaseCtx.fixContext;
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
findPreviousAiPhaseIndex(phases, currentIdx) {
|
|
4536
|
+
for (let j = currentIdx - 1; j >= 0; j--) {
|
|
4537
|
+
if (phases[j].kind === "ai") return j;
|
|
4538
|
+
}
|
|
4539
|
+
return -1;
|
|
4540
|
+
}
|
|
4541
|
+
};
|
|
4542
|
+
|
|
4543
|
+
// src/orchestrator/strategies/index.ts
|
|
4544
|
+
var gateStrategy = new GateStrategy();
|
|
4545
|
+
var aiStrategy = new AiPhaseStrategy();
|
|
4546
|
+
var verifyFixStrategy = new VerifyFixStrategy();
|
|
4547
|
+
function resolveStrategy(spec, config) {
|
|
4548
|
+
if (spec.kind === "gate") {
|
|
4549
|
+
return gateStrategy;
|
|
4550
|
+
}
|
|
4551
|
+
if (spec.name === "verify" && config.verifyFixLoop.enabled) {
|
|
4552
|
+
return verifyFixStrategy;
|
|
4553
|
+
}
|
|
4554
|
+
return aiStrategy;
|
|
4555
|
+
}
|
|
4556
|
+
|
|
4557
|
+
// src/orchestrator/steps/PhaseLoopStep.ts
|
|
4558
|
+
var logger20 = logger.child("PhaseLoopStep");
|
|
4559
|
+
async function runPhaseWithLifecycle(phase, phaseCtx, spec, ctx, deps, wtGit, wtPlan, wtGitMap, hook) {
|
|
4560
|
+
const lifecycleHook = hook ?? new DefaultLifecycleHook();
|
|
4561
|
+
const execCtx = {
|
|
4562
|
+
spec,
|
|
4563
|
+
issueCtx: ctx,
|
|
4564
|
+
deps,
|
|
4565
|
+
wtGit,
|
|
4566
|
+
wtPlan,
|
|
4567
|
+
wtGitMap,
|
|
4568
|
+
phase,
|
|
4569
|
+
displayId: ctx.issue.iid
|
|
4570
|
+
};
|
|
4571
|
+
await lifecycleHook.beforePhase(execCtx);
|
|
4572
|
+
if (lifecycleHook.beforeExecute) {
|
|
4573
|
+
await lifecycleHook.beforeExecute(execCtx);
|
|
4574
|
+
}
|
|
4575
|
+
const callbacks = createCallbacksFromHook(lifecycleHook, execCtx);
|
|
4576
|
+
const outcome = await phase.run(phaseCtx, callbacks);
|
|
4577
|
+
if (outcome.sessionId) wtPlan.updatePhaseSessionId(spec.name, outcome.sessionId);
|
|
4578
|
+
const finalOutcome = lifecycleHook.afterExecute ? await lifecycleHook.afterExecute(execCtx, outcome) : outcome;
|
|
4579
|
+
if (finalOutcome.status === "completed") {
|
|
4580
|
+
await lifecycleHook.afterPhase(execCtx, finalOutcome);
|
|
4581
|
+
return finalOutcome;
|
|
4582
|
+
}
|
|
4583
|
+
if (finalOutcome.status === "running") {
|
|
4584
|
+
return finalOutcome;
|
|
4585
|
+
}
|
|
4586
|
+
const errMsg = finalOutcome.error?.message ?? "Unknown error";
|
|
4587
|
+
const phaseError = {
|
|
4588
|
+
message: errMsg,
|
|
4589
|
+
isRetryable: finalOutcome.error?.isRetryable ?? true,
|
|
4590
|
+
rawOutput: finalOutcome.error?.rawOutput ?? finalOutcome.output,
|
|
4591
|
+
wasActiveAtTimeout: finalOutcome.error?.wasActiveAtTimeout ?? false
|
|
4592
|
+
};
|
|
4593
|
+
await lifecycleHook.onError(execCtx, phaseError);
|
|
4594
|
+
const shortErr = errMsg.slice(0, 200);
|
|
4606
4595
|
throw new AIExecutionError(spec.name, `Phase ${spec.name} failed: ${shortErr}`, {
|
|
4607
|
-
output:
|
|
4608
|
-
exitCode:
|
|
4609
|
-
isRetryable:
|
|
4610
|
-
wasActiveAtTimeout:
|
|
4596
|
+
output: finalOutcome.error?.rawOutput ?? finalOutcome.output,
|
|
4597
|
+
exitCode: finalOutcome.exitCode ?? 1,
|
|
4598
|
+
isRetryable: finalOutcome.error?.isRetryable,
|
|
4599
|
+
wasActiveAtTimeout: finalOutcome.error?.wasActiveAtTimeout ?? false
|
|
4611
4600
|
});
|
|
4612
4601
|
}
|
|
4613
4602
|
async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
@@ -4631,15 +4620,15 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
|
4631
4620
|
if (skippedDeployPhase && !phaseCtx.ports) {
|
|
4632
4621
|
const existingPorts = deps.getPortsForIssue(issue.iid);
|
|
4633
4622
|
if (existingPorts && deps.isPreviewRunning(issue.iid)) {
|
|
4634
|
-
|
|
4623
|
+
logger20.info("Restored preview ports from allocator", { iid: issue.iid, ...existingPorts });
|
|
4635
4624
|
phaseCtx.ports = existingPorts;
|
|
4636
4625
|
ctx.wtCtx.ports = existingPorts;
|
|
4637
4626
|
serversStarted = true;
|
|
4638
4627
|
} else {
|
|
4639
4628
|
if (existingPorts) {
|
|
4640
|
-
|
|
4629
|
+
logger20.info("Ports allocated but servers not running, restarting", { iid: issue.iid });
|
|
4641
4630
|
} else {
|
|
4642
|
-
|
|
4631
|
+
logger20.info("Restarting preview servers for resumed pipeline", { iid: issue.iid });
|
|
4643
4632
|
}
|
|
4644
4633
|
const ports = await deps.startPreviewServers(ctx.wtCtx, issue);
|
|
4645
4634
|
if (ports) {
|
|
@@ -4651,93 +4640,25 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
|
4651
4640
|
}
|
|
4652
4641
|
}
|
|
4653
4642
|
if (startIdx > 0) {
|
|
4654
|
-
|
|
4655
|
-
if (currentProgress) {
|
|
4656
|
-
let patched = false;
|
|
4657
|
-
for (let i = 0; i < startIdx; i++) {
|
|
4658
|
-
const prevSpec = pipelineDef.phases[i];
|
|
4659
|
-
const pp = currentProgress.phases[prevSpec.name];
|
|
4660
|
-
if (pp && pp.status !== "completed") {
|
|
4661
|
-
logger17.warn("Fixing stale phase progress", {
|
|
4662
|
-
iid: issue.iid,
|
|
4663
|
-
phase: prevSpec.name,
|
|
4664
|
-
was: pp.status,
|
|
4665
|
-
now: "completed"
|
|
4666
|
-
});
|
|
4667
|
-
pp.status = "completed";
|
|
4668
|
-
if (!pp.completedAt) {
|
|
4669
|
-
pp.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4670
|
-
}
|
|
4671
|
-
patched = true;
|
|
4672
|
-
}
|
|
4673
|
-
}
|
|
4674
|
-
if (patched) {
|
|
4675
|
-
wtPlan.writeProgress(currentProgress);
|
|
4676
|
-
}
|
|
4677
|
-
}
|
|
4678
|
-
if (record.phaseProgress) {
|
|
4679
|
-
for (let i = 0; i < startIdx; i++) {
|
|
4680
|
-
const prevSpec = pipelineDef.phases[i];
|
|
4681
|
-
const tp = record.phaseProgress[prevSpec.name];
|
|
4682
|
-
if (tp && tp.status !== "completed") {
|
|
4683
|
-
deps.tracker.updatePhaseProgress(issue.iid, prevSpec.name, {
|
|
4684
|
-
status: "completed",
|
|
4685
|
-
completedAt: tp.completedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
4686
|
-
});
|
|
4687
|
-
}
|
|
4688
|
-
}
|
|
4689
|
-
}
|
|
4643
|
+
healStaleProgress(ctx, deps, wtPlan, startIdx);
|
|
4690
4644
|
}
|
|
4645
|
+
const hooks = new DefaultLifecycleHook();
|
|
4691
4646
|
for (let i = startIdx; i < pipelineDef.phases.length; i++) {
|
|
4692
|
-
if (isShuttingDown())
|
|
4693
|
-
throw new ServiceShutdownError();
|
|
4694
|
-
}
|
|
4647
|
+
if (isShuttingDown()) throw new ServiceShutdownError();
|
|
4695
4648
|
const spec = pipelineDef.phases[i];
|
|
4696
4649
|
const pendingAction = deps.consumePendingAction?.(issue.iid);
|
|
4697
|
-
if (pendingAction)
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
}
|
|
4710
|
-
wtPlan.updatePhaseProgress(spec.name, "completed");
|
|
4711
|
-
deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
|
|
4712
|
-
status: "completed",
|
|
4713
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4714
|
-
});
|
|
4715
|
-
try {
|
|
4716
|
-
await deps.gongfeng.createIssueNote(
|
|
4717
|
-
issue.id,
|
|
4718
|
-
t("orchestrator.autoApproveComment")
|
|
4719
|
-
);
|
|
4720
|
-
} catch {
|
|
4721
|
-
}
|
|
4722
|
-
continue;
|
|
4723
|
-
}
|
|
4724
|
-
deps.tracker.updateState(issue.iid, spec.startState, { currentPhase: spec.name });
|
|
4725
|
-
wtPlan.updatePhaseProgress(spec.name, "in_progress");
|
|
4726
|
-
deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
|
|
4727
|
-
status: "in_progress",
|
|
4728
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4729
|
-
});
|
|
4730
|
-
deps.eventBus.emitTyped("review:requested", { issueIid: issue.iid });
|
|
4731
|
-
logger17.info("Review gate reached, pausing", { iid: issue.iid });
|
|
4732
|
-
return { serversStarted, paused: true };
|
|
4733
|
-
}
|
|
4734
|
-
if (spec.name === "verify" && deps.config.verifyFixLoop.enabled) {
|
|
4735
|
-
const buildIdx = findPreviousAiPhaseIndex(pipelineDef.phases, i);
|
|
4736
|
-
await executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, i, buildIdx, wtGitMap);
|
|
4737
|
-
continue;
|
|
4738
|
-
}
|
|
4739
|
-
if (spec.name === "uat" && !isE2eEnabledForIssue(issue.iid, deps.tracker, deps.config)) {
|
|
4740
|
-
logger17.info("UAT phase skipped (E2E not enabled for this issue)", { iid: issue.iid });
|
|
4650
|
+
if (pendingAction) throw new PhaseAbortedError(spec.name, pendingAction);
|
|
4651
|
+
const strategy = resolveStrategy(spec, deps.config);
|
|
4652
|
+
const execCtx = {
|
|
4653
|
+
spec,
|
|
4654
|
+
issueCtx: ctx,
|
|
4655
|
+
deps,
|
|
4656
|
+
wtGit,
|
|
4657
|
+
wtPlan,
|
|
4658
|
+
wtGitMap,
|
|
4659
|
+
displayId: issue.iid
|
|
4660
|
+
};
|
|
4661
|
+
if (strategy.shouldSkip(execCtx)) {
|
|
4741
4662
|
deps.tracker.updateState(issue.iid, spec.doneState, { currentPhase: spec.name });
|
|
4742
4663
|
wtPlan.updatePhaseProgress(spec.name, "completed");
|
|
4743
4664
|
deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
|
|
@@ -4746,49 +4667,12 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
|
4746
4667
|
});
|
|
4747
4668
|
continue;
|
|
4748
4669
|
}
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
const runnerName = runner === deps.e2eAiRunner ? "e2eAiRunner (CodeBuddy)" : "mainRunner";
|
|
4753
|
-
logger17.info("UAT phase starting", { iid: issue.iid, runner: runnerName });
|
|
4754
|
-
}
|
|
4755
|
-
const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config);
|
|
4756
|
-
if (wtGitMap) {
|
|
4757
|
-
phase.setWtGitMap(wtGitMap);
|
|
4758
|
-
}
|
|
4759
|
-
const outcome = await runPhaseWithLifecycle(
|
|
4760
|
-
phase,
|
|
4761
|
-
phaseCtx,
|
|
4762
|
-
spec,
|
|
4763
|
-
ctx,
|
|
4764
|
-
deps,
|
|
4765
|
-
wtGit,
|
|
4766
|
-
wtPlan,
|
|
4767
|
-
wtGitMap
|
|
4768
|
-
);
|
|
4769
|
-
if (outcome.status === "running") {
|
|
4770
|
-
if (outcome.awaitCompletion) {
|
|
4771
|
-
logger17.info("Async phase running, awaiting completion", { iid: issue.iid, phase: spec.name });
|
|
4772
|
-
const finalOutcome = await awaitAsyncPhase(outcome, spec, ctx, deps, wtGit, wtPlan, wtGitMap);
|
|
4773
|
-
if (finalOutcome.status === "completed") {
|
|
4774
|
-
continue;
|
|
4775
|
-
}
|
|
4776
|
-
}
|
|
4777
|
-
deps.tracker.updateState(issue.iid, "phase_waiting" /* PhaseWaiting */, { currentPhase: spec.name });
|
|
4778
|
-
wtPlan.updatePhaseProgress(spec.name, "gate_waiting");
|
|
4779
|
-
deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "gate_waiting" });
|
|
4780
|
-
const gateEvent = spec.name === "uat" ? "uat:gateRequested" : "release:gateRequested";
|
|
4781
|
-
deps.eventBus.emitTyped(gateEvent, { issueIid: issue.iid });
|
|
4782
|
-
logger17.info("Async phase running (no awaitCompletion), pausing pipeline", { iid: issue.iid, phase: spec.name });
|
|
4783
|
-
return { serversStarted, paused: true };
|
|
4670
|
+
const result = await strategy.execute(execCtx, hooks);
|
|
4671
|
+
if (result.paused) {
|
|
4672
|
+
return { serversStarted: serversStarted || !!result.serversStarted, paused: true };
|
|
4784
4673
|
}
|
|
4785
|
-
if (
|
|
4786
|
-
|
|
4787
|
-
wtPlan.updatePhaseProgress(spec.name, "gate_waiting");
|
|
4788
|
-
deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "gate_waiting" });
|
|
4789
|
-
deps.eventBus.emitTyped("release:gateRequested", { issueIid: issue.iid });
|
|
4790
|
-
logger17.info("Phase requested gate, pausing", { iid: issue.iid, phase: spec.name });
|
|
4791
|
-
return { serversStarted, paused: true };
|
|
4674
|
+
if (result.serversStarted) {
|
|
4675
|
+
serversStarted = true;
|
|
4792
4676
|
}
|
|
4793
4677
|
if (needsDeployment && !serversStarted && lifecycleManager.shouldDeployPreview(spec.name)) {
|
|
4794
4678
|
const ports = await deps.startPreviewServers(ctx.wtCtx, issue);
|
|
@@ -4801,191 +4685,48 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
|
4801
4685
|
}
|
|
4802
4686
|
return { serversStarted, paused: false };
|
|
4803
4687
|
}
|
|
4804
|
-
|
|
4805
|
-
const { issue } = ctx;
|
|
4806
|
-
const
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
return finalOutcome;
|
|
4825
|
-
}
|
|
4826
|
-
const errMsg = finalOutcome.error?.message ?? "Unknown error";
|
|
4827
|
-
const shortErr = errMsg.slice(0, 200);
|
|
4828
|
-
const wasActive = finalOutcome.error?.wasActiveAtTimeout ?? false;
|
|
4829
|
-
wtPlan.updatePhaseProgress(spec.name, "failed", errMsg);
|
|
4830
|
-
deps.tracker.updatePhaseProgress(displayId, spec.name, { status: "failed" });
|
|
4831
|
-
if (wasActive) {
|
|
4832
|
-
deps.tracker.markFailedSoft(displayId, errMsg, "phase_running" /* PhaseRunning */);
|
|
4833
|
-
} else {
|
|
4834
|
-
deps.tracker.markFailed(displayId, errMsg, "phase_running" /* PhaseRunning */);
|
|
4835
|
-
}
|
|
4836
|
-
await safeComment(deps, issue.id, issueProgressComment(spec.name, "failed", shortErr));
|
|
4837
|
-
throw new AIExecutionError(spec.name, `Phase ${spec.name} failed: ${shortErr}`, {
|
|
4838
|
-
output: finalOutcome.error?.rawOutput ?? finalOutcome.output,
|
|
4839
|
-
exitCode: finalOutcome.exitCode ?? 1,
|
|
4840
|
-
isRetryable: finalOutcome.error?.isRetryable,
|
|
4841
|
-
wasActiveAtTimeout: wasActive
|
|
4842
|
-
});
|
|
4843
|
-
}
|
|
4844
|
-
function findPreviousAiPhaseIndex(phases, currentIdx) {
|
|
4845
|
-
for (let j = currentIdx - 1; j >= 0; j--) {
|
|
4846
|
-
if (phases[j].kind === "ai") return j;
|
|
4847
|
-
}
|
|
4848
|
-
return -1;
|
|
4849
|
-
}
|
|
4850
|
-
async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, buildPhaseIdx, wtGitMap) {
|
|
4851
|
-
const { issue, lifecycleManager, phaseCtx } = ctx;
|
|
4852
|
-
const maxIterations = deps.config.verifyFixLoop.maxIterations;
|
|
4853
|
-
const verifySpec = ctx.pipelineDef.phases[verifyPhaseIdx];
|
|
4854
|
-
deps.eventBus.emitTyped("verify:loopStarted", {
|
|
4855
|
-
issueIid: issue.iid,
|
|
4856
|
-
maxIterations
|
|
4857
|
-
});
|
|
4858
|
-
logger17.info("Verify-fix loop started", {
|
|
4859
|
-
iid: issue.iid,
|
|
4860
|
-
maxIterations,
|
|
4861
|
-
buildPhaseIdx
|
|
4862
|
-
});
|
|
4863
|
-
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
4864
|
-
if (isShuttingDown()) {
|
|
4865
|
-
throw new ServiceShutdownError();
|
|
4866
|
-
}
|
|
4867
|
-
logger17.info("Verify-fix loop iteration", {
|
|
4868
|
-
iteration,
|
|
4869
|
-
maxIterations,
|
|
4870
|
-
iid: issue.iid
|
|
4871
|
-
});
|
|
4872
|
-
updateHooksForPhase(verifySpec, ctx.pipelineDef, ctx, wtPlan);
|
|
4873
|
-
const verifyRunner = resolveVerifyRunner(deps);
|
|
4874
|
-
const verifyPhase = createPhase("verify", verifyRunner, wtGit, wtPlan, deps.config);
|
|
4875
|
-
if (wtGitMap) {
|
|
4876
|
-
verifyPhase.setWtGitMap(wtGitMap);
|
|
4877
|
-
}
|
|
4878
|
-
let verifyOutcome;
|
|
4879
|
-
try {
|
|
4880
|
-
verifyOutcome = await runPhaseWithLifecycle(
|
|
4881
|
-
verifyPhase,
|
|
4882
|
-
phaseCtx,
|
|
4883
|
-
verifySpec,
|
|
4884
|
-
ctx,
|
|
4885
|
-
deps,
|
|
4886
|
-
wtGit,
|
|
4887
|
-
wtPlan,
|
|
4888
|
-
wtGitMap
|
|
4889
|
-
);
|
|
4890
|
-
} catch (err) {
|
|
4891
|
-
logger17.warn("Verify phase execution failed", {
|
|
4892
|
-
iteration,
|
|
4893
|
-
iid: issue.iid,
|
|
4894
|
-
error: err.message
|
|
4895
|
-
});
|
|
4896
|
-
deps.eventBus.emitTyped("verify:iterationComplete", {
|
|
4897
|
-
issueIid: issue.iid,
|
|
4898
|
-
iteration,
|
|
4899
|
-
passed: false,
|
|
4900
|
-
failures: ["AI runner execution failed"]
|
|
4901
|
-
});
|
|
4902
|
-
if (iteration === maxIterations) {
|
|
4903
|
-
throw err;
|
|
4904
|
-
}
|
|
4905
|
-
if (buildPhaseIdx >= 0) {
|
|
4906
|
-
await executeBuildFix(ctx, deps, wtGit, wtPlan, buildPhaseIdx, {
|
|
4907
|
-
iteration,
|
|
4908
|
-
verifyFailures: ["AI runner execution failed: " + err.message],
|
|
4909
|
-
rawReport: ""
|
|
4910
|
-
}, wtGitMap);
|
|
4688
|
+
function healStaleProgress(ctx, deps, wtPlan, startIdx) {
|
|
4689
|
+
const { issue, pipelineDef, record } = ctx;
|
|
4690
|
+
const currentProgress = wtPlan.readProgress();
|
|
4691
|
+
if (currentProgress) {
|
|
4692
|
+
let patched = false;
|
|
4693
|
+
for (let i = 0; i < startIdx; i++) {
|
|
4694
|
+
const prevSpec = pipelineDef.phases[i];
|
|
4695
|
+
const pp = currentProgress.phases[prevSpec.name];
|
|
4696
|
+
if (pp && pp.status !== "completed") {
|
|
4697
|
+
logger20.warn("Fixing stale phase progress", {
|
|
4698
|
+
iid: issue.iid,
|
|
4699
|
+
phase: prevSpec.name,
|
|
4700
|
+
was: pp.status,
|
|
4701
|
+
now: "completed"
|
|
4702
|
+
});
|
|
4703
|
+
pp.status = "completed";
|
|
4704
|
+
if (!pp.completedAt) {
|
|
4705
|
+
pp.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4706
|
+
}
|
|
4707
|
+
patched = true;
|
|
4911
4708
|
}
|
|
4912
|
-
continue;
|
|
4913
|
-
}
|
|
4914
|
-
const report = verifyOutcome.data?.verifyReport;
|
|
4915
|
-
const passed = report ? report.passed : true;
|
|
4916
|
-
deps.eventBus.emitTyped("verify:iterationComplete", {
|
|
4917
|
-
issueIid: issue.iid,
|
|
4918
|
-
iteration,
|
|
4919
|
-
passed,
|
|
4920
|
-
failures: report?.failureReasons
|
|
4921
|
-
});
|
|
4922
|
-
if (passed) {
|
|
4923
|
-
logger17.info("Verify-fix loop passed", {
|
|
4924
|
-
iteration,
|
|
4925
|
-
iid: issue.iid
|
|
4926
|
-
});
|
|
4927
|
-
return;
|
|
4928
4709
|
}
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
iid: issue.iid,
|
|
4932
|
-
failures: report?.failureReasons,
|
|
4933
|
-
todolistStats: report?.todolistStats
|
|
4934
|
-
});
|
|
4935
|
-
if (iteration === maxIterations) {
|
|
4936
|
-
deps.eventBus.emitTyped("verify:loopExhausted", {
|
|
4937
|
-
issueIid: issue.iid,
|
|
4938
|
-
totalIterations: iteration,
|
|
4939
|
-
failures: report?.failureReasons ?? []
|
|
4940
|
-
});
|
|
4941
|
-
const failMsg = `Verify-fix loop exhausted after ${maxIterations} iterations. Remaining issues: ${report?.failureReasons?.join("; ") ?? "unknown"}`;
|
|
4942
|
-
logger17.warn(failMsg, { iid: issue.iid });
|
|
4943
|
-
throw new AIExecutionError("verify", failMsg, {
|
|
4944
|
-
output: report?.rawReport ?? "",
|
|
4945
|
-
exitCode: 0
|
|
4946
|
-
});
|
|
4947
|
-
}
|
|
4948
|
-
if (buildPhaseIdx >= 0) {
|
|
4949
|
-
await executeBuildFix(ctx, deps, wtGit, wtPlan, buildPhaseIdx, {
|
|
4950
|
-
iteration,
|
|
4951
|
-
verifyFailures: report?.failureReasons ?? [],
|
|
4952
|
-
rawReport: report?.rawReport ?? ""
|
|
4953
|
-
}, wtGitMap);
|
|
4710
|
+
if (patched) {
|
|
4711
|
+
wtPlan.writeProgress(currentProgress);
|
|
4954
4712
|
}
|
|
4955
4713
|
}
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
try {
|
|
4967
|
-
updateHooksForPhase(buildSpec, ctx.pipelineDef, ctx, wtPlan);
|
|
4968
|
-
const buildPhase = createPhase("build", deps.aiRunner, wtGit, wtPlan, deps.config);
|
|
4969
|
-
if (wtGitMap) {
|
|
4970
|
-
buildPhase.setWtGitMap(wtGitMap);
|
|
4714
|
+
if (record.phaseProgress) {
|
|
4715
|
+
for (let i = 0; i < startIdx; i++) {
|
|
4716
|
+
const prevSpec = pipelineDef.phases[i];
|
|
4717
|
+
const tp = record.phaseProgress[prevSpec.name];
|
|
4718
|
+
if (tp && tp.status !== "completed") {
|
|
4719
|
+
deps.tracker.updatePhaseProgress(issue.iid, prevSpec.name, {
|
|
4720
|
+
status: "completed",
|
|
4721
|
+
completedAt: tp.completedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
4722
|
+
});
|
|
4723
|
+
}
|
|
4971
4724
|
}
|
|
4972
|
-
await runPhaseWithLifecycle(
|
|
4973
|
-
buildPhase,
|
|
4974
|
-
phaseCtx,
|
|
4975
|
-
buildSpec,
|
|
4976
|
-
ctx,
|
|
4977
|
-
deps,
|
|
4978
|
-
wtGit,
|
|
4979
|
-
wtPlan,
|
|
4980
|
-
wtGitMap
|
|
4981
|
-
);
|
|
4982
|
-
} finally {
|
|
4983
|
-
delete phaseCtx.fixContext;
|
|
4984
4725
|
}
|
|
4985
4726
|
}
|
|
4986
4727
|
|
|
4987
4728
|
// src/orchestrator/steps/CompletionStep.ts
|
|
4988
|
-
var
|
|
4729
|
+
var logger21 = logger.child("CompletionStep");
|
|
4989
4730
|
async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
|
|
4990
4731
|
const { issue, branchName, wtCtx } = ctx;
|
|
4991
4732
|
deps.emitProgress(issue.iid, "create_mr", t("orchestrator.createMrProgress"));
|
|
@@ -5017,7 +4758,7 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
|
|
|
5017
4758
|
mrIid: void 0
|
|
5018
4759
|
});
|
|
5019
4760
|
} catch (err) {
|
|
5020
|
-
|
|
4761
|
+
logger21.warn("Failed to publish E2E screenshots", {
|
|
5021
4762
|
iid: issue.iid,
|
|
5022
4763
|
error: err.message
|
|
5023
4764
|
});
|
|
@@ -5037,19 +4778,19 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
|
|
|
5037
4778
|
await deps.claimer.releaseClaim(issue.id, issue.iid, "completed");
|
|
5038
4779
|
}
|
|
5039
4780
|
if (phaseResult.serversStarted && deps.config.preview.keepAfterComplete) {
|
|
5040
|
-
|
|
4781
|
+
logger21.info("Preview servers kept running after completion", { iid: issue.iid });
|
|
5041
4782
|
} else {
|
|
5042
4783
|
deps.stopPreviewServers(issue.iid);
|
|
5043
4784
|
await deps.mainGitMutex.runExclusive(async () => {
|
|
5044
4785
|
if (wtCtx.workspace) {
|
|
5045
4786
|
await deps.workspaceManager.cleanupWorkspace(wtCtx.workspace);
|
|
5046
|
-
|
|
4787
|
+
logger21.info("Workspace cleaned up", { dir: wtCtx.workspace.workspaceRoot });
|
|
5047
4788
|
} else {
|
|
5048
4789
|
try {
|
|
5049
4790
|
await deps.mainGit.worktreeRemove(wtCtx.gitRootDir, true);
|
|
5050
|
-
|
|
4791
|
+
logger21.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
|
|
5051
4792
|
} catch (err) {
|
|
5052
|
-
|
|
4793
|
+
logger21.warn("Failed to cleanup worktree", {
|
|
5053
4794
|
dir: wtCtx.gitRootDir,
|
|
5054
4795
|
error: err.message
|
|
5055
4796
|
});
|
|
@@ -5057,16 +4798,16 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
|
|
|
5057
4798
|
}
|
|
5058
4799
|
});
|
|
5059
4800
|
}
|
|
5060
|
-
|
|
4801
|
+
logger21.info("Issue processing completed", { iid: issue.iid });
|
|
5061
4802
|
}
|
|
5062
4803
|
|
|
5063
4804
|
// src/orchestrator/steps/FailureHandler.ts
|
|
5064
|
-
var
|
|
4805
|
+
var logger22 = logger.child("FailureHandler");
|
|
5065
4806
|
async function handleFailure(err, issue, wtCtx, deps) {
|
|
5066
4807
|
const errorMsg = err.message;
|
|
5067
4808
|
const isRetryable = err instanceof AIExecutionError ? err.isRetryable : true;
|
|
5068
4809
|
const wasActiveAtTimeout = err instanceof AIExecutionError && err.wasActiveAtTimeout;
|
|
5069
|
-
|
|
4810
|
+
logger22.error("Issue processing failed", { iid: issue.iid, error: errorMsg, isRetryable, wasActiveAtTimeout });
|
|
5070
4811
|
metrics.incCounter("iaf_issues_failed_total");
|
|
5071
4812
|
const currentRecord = deps.tracker.get(issue.iid);
|
|
5072
4813
|
const failedAtState = currentRecord?.state || "pending" /* Pending */;
|
|
@@ -5079,11 +4820,11 @@ async function handleFailure(err, issue, wtCtx, deps) {
|
|
|
5079
4820
|
}
|
|
5080
4821
|
}
|
|
5081
4822
|
if (wasReset) {
|
|
5082
|
-
|
|
4823
|
+
logger22.info("Issue was reset during processing, skipping failure marking", { iid: issue.iid });
|
|
5083
4824
|
throw err;
|
|
5084
4825
|
}
|
|
5085
4826
|
if (failedAtState === "paused" /* Paused */) {
|
|
5086
|
-
|
|
4827
|
+
logger22.info("Issue was paused during processing, skipping failure handling", { iid: issue.iid });
|
|
5087
4828
|
throw err;
|
|
5088
4829
|
}
|
|
5089
4830
|
try {
|
|
@@ -5105,7 +4846,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
|
|
|
5105
4846
|
try {
|
|
5106
4847
|
await deps.claimer.releaseClaim(issue.id, issue.iid, "failed");
|
|
5107
4848
|
} catch (releaseErr) {
|
|
5108
|
-
|
|
4849
|
+
logger22.warn("Failed to release lock on failure", {
|
|
5109
4850
|
iid: issue.iid,
|
|
5110
4851
|
error: releaseErr.message
|
|
5111
4852
|
});
|
|
@@ -5113,7 +4854,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
|
|
|
5113
4854
|
}
|
|
5114
4855
|
deps.stopPreviewServers(issue.iid);
|
|
5115
4856
|
const preservedDirs = wtCtx.workspace ? [wtCtx.workspace.primary.gitRootDir, ...wtCtx.workspace.associates.map((a) => a.gitRootDir)] : [wtCtx.gitRootDir];
|
|
5116
|
-
|
|
4857
|
+
logger22.info("Worktree(s) preserved for debugging", {
|
|
5117
4858
|
primary: wtCtx.gitRootDir,
|
|
5118
4859
|
all: preservedDirs
|
|
5119
4860
|
});
|
|
@@ -5122,7 +4863,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
|
|
|
5122
4863
|
|
|
5123
4864
|
// src/orchestrator/PipelineOrchestrator.ts
|
|
5124
4865
|
var execFileAsync2 = promisify2(execFile2);
|
|
5125
|
-
var
|
|
4866
|
+
var logger23 = logger.child("PipelineOrchestrator");
|
|
5126
4867
|
var PipelineOrchestrator = class {
|
|
5127
4868
|
config;
|
|
5128
4869
|
gongfeng;
|
|
@@ -5152,7 +4893,7 @@ var PipelineOrchestrator = class {
|
|
|
5152
4893
|
setAIRunner(runner) {
|
|
5153
4894
|
this.aiRunner = runner;
|
|
5154
4895
|
this.conflictResolver = new ConflictResolver(runner);
|
|
5155
|
-
|
|
4896
|
+
logger23.info("AIRunner replaced via hot-reload");
|
|
5156
4897
|
}
|
|
5157
4898
|
constructor(config, gongfeng, git, aiRunner, tracker, supplementStore, mainGitMutex, eventBusInstance, wsConfig, tenantId, e2eAiRunner) {
|
|
5158
4899
|
this.config = config;
|
|
@@ -5170,14 +4911,14 @@ var PipelineOrchestrator = class {
|
|
|
5170
4911
|
this.pipelineDef = mode === "plan-mode" ? buildPlanModePipeline({ releaseEnabled: config.release.enabled, e2eEnabled: config.e2e.enabled }) : getPipelineDef(mode);
|
|
5171
4912
|
registerPipeline(this.pipelineDef);
|
|
5172
4913
|
this.lifecycleManager = createLifecycleManager(this.pipelineDef);
|
|
5173
|
-
|
|
4914
|
+
logger23.info("Pipeline mode resolved", { tenantId: this.tenantId, mode: this.pipelineDef.mode, aiMode: config.ai.mode });
|
|
5174
4915
|
this.portAllocator = new PortAllocator({
|
|
5175
4916
|
backendPortBase: config.e2e.backendPortBase,
|
|
5176
4917
|
frontendPortBase: config.e2e.frontendPortBase
|
|
5177
4918
|
});
|
|
5178
4919
|
this.devServerManager = new DevServerManager();
|
|
5179
4920
|
this.screenshotPublisher = new ScreenshotPublisher(gongfeng);
|
|
5180
|
-
this.effectiveWorktreeBaseDir = this.tenantId === "default" ? config.project.worktreeBaseDir :
|
|
4921
|
+
this.effectiveWorktreeBaseDir = this.tenantId === "default" ? config.project.worktreeBaseDir : path11.join(config.project.worktreeBaseDir, this.tenantId);
|
|
5181
4922
|
const effectiveWsConfig = wsConfig ?? buildSingleRepoWorkspace(config.project, config.gongfeng.projectPath);
|
|
5182
4923
|
this.workspaceManager = new WorkspaceManager({
|
|
5183
4924
|
wsConfig: effectiveWsConfig,
|
|
@@ -5186,7 +4927,7 @@ var PipelineOrchestrator = class {
|
|
|
5186
4927
|
mainGitMutex: this.mainGitMutex,
|
|
5187
4928
|
gongfengApiUrl: config.gongfeng.apiUrl
|
|
5188
4929
|
});
|
|
5189
|
-
|
|
4930
|
+
logger23.info("WorkspaceManager initialized", {
|
|
5190
4931
|
tenantId: this.tenantId,
|
|
5191
4932
|
primary: effectiveWsConfig.primary.name,
|
|
5192
4933
|
associates: effectiveWsConfig.associates.map((a) => a.name)
|
|
@@ -5207,7 +4948,7 @@ var PipelineOrchestrator = class {
|
|
|
5207
4948
|
this.claimer = claimer;
|
|
5208
4949
|
}
|
|
5209
4950
|
async cleanupStaleState() {
|
|
5210
|
-
|
|
4951
|
+
logger23.info("Cleaning up stale worktree state...");
|
|
5211
4952
|
let cleaned = 0;
|
|
5212
4953
|
const repoGitRoot = this.config.project.gitRootDir;
|
|
5213
4954
|
try {
|
|
@@ -5216,11 +4957,11 @@ var PipelineOrchestrator = class {
|
|
|
5216
4957
|
if (wtDir === repoGitRoot) continue;
|
|
5217
4958
|
if (!wtDir.includes("/issue-")) continue;
|
|
5218
4959
|
try {
|
|
5219
|
-
const gitFile =
|
|
4960
|
+
const gitFile = path11.join(wtDir, ".git");
|
|
5220
4961
|
try {
|
|
5221
|
-
await
|
|
4962
|
+
await fs9.access(gitFile);
|
|
5222
4963
|
} catch {
|
|
5223
|
-
|
|
4964
|
+
logger23.warn("Worktree corrupted (.git missing), force removing", { dir: wtDir });
|
|
5224
4965
|
await this.mainGit.worktreeRemove(wtDir, true).catch(() => {
|
|
5225
4966
|
});
|
|
5226
4967
|
await this.mainGit.worktreePrune();
|
|
@@ -5229,32 +4970,32 @@ var PipelineOrchestrator = class {
|
|
|
5229
4970
|
}
|
|
5230
4971
|
const wtGit = new GitOperations(wtDir);
|
|
5231
4972
|
if (await wtGit.isRebaseInProgress()) {
|
|
5232
|
-
|
|
4973
|
+
logger23.warn("Aborting residual rebase in worktree", { dir: wtDir });
|
|
5233
4974
|
await wtGit.rebaseAbort();
|
|
5234
4975
|
cleaned++;
|
|
5235
4976
|
}
|
|
5236
|
-
const indexLock =
|
|
4977
|
+
const indexLock = path11.join(wtDir, ".git", "index.lock");
|
|
5237
4978
|
try {
|
|
5238
|
-
await
|
|
5239
|
-
|
|
4979
|
+
await fs9.unlink(indexLock);
|
|
4980
|
+
logger23.warn("Removed stale index.lock", { path: indexLock });
|
|
5240
4981
|
cleaned++;
|
|
5241
4982
|
} catch {
|
|
5242
4983
|
}
|
|
5243
4984
|
} catch (err) {
|
|
5244
|
-
|
|
4985
|
+
logger23.warn("Failed to clean worktree state", { dir: wtDir, error: err.message });
|
|
5245
4986
|
}
|
|
5246
4987
|
}
|
|
5247
4988
|
} catch (err) {
|
|
5248
|
-
|
|
4989
|
+
logger23.warn("Failed to list worktrees for cleanup", { error: err.message });
|
|
5249
4990
|
}
|
|
5250
|
-
const mainIndexLock =
|
|
4991
|
+
const mainIndexLock = path11.join(repoGitRoot, ".git", "index.lock");
|
|
5251
4992
|
try {
|
|
5252
|
-
await
|
|
5253
|
-
|
|
4993
|
+
await fs9.unlink(mainIndexLock);
|
|
4994
|
+
logger23.warn("Removed stale main repo index.lock", { path: mainIndexLock });
|
|
5254
4995
|
cleaned++;
|
|
5255
4996
|
} catch {
|
|
5256
4997
|
}
|
|
5257
|
-
|
|
4998
|
+
logger23.info("Stale state cleanup complete", { cleaned });
|
|
5258
4999
|
}
|
|
5259
5000
|
/**
|
|
5260
5001
|
* 重启后清理幽灵端口分配。
|
|
@@ -5267,7 +5008,7 @@ var PipelineOrchestrator = class {
|
|
|
5267
5008
|
for (const record of this.tracker.getAll()) {
|
|
5268
5009
|
if (record.ports) {
|
|
5269
5010
|
const iid = getIid(record);
|
|
5270
|
-
|
|
5011
|
+
logger23.info("Clearing stale port allocation after restart", { iid, ports: record.ports });
|
|
5271
5012
|
this.tracker.updateState(iid, record.state, {
|
|
5272
5013
|
ports: void 0,
|
|
5273
5014
|
previewStartedAt: void 0
|
|
@@ -5312,20 +5053,20 @@ var PipelineOrchestrator = class {
|
|
|
5312
5053
|
}
|
|
5313
5054
|
try {
|
|
5314
5055
|
await this.mainGit.worktreeRemove(wtCtx.gitRootDir, true);
|
|
5315
|
-
|
|
5056
|
+
logger23.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
|
|
5316
5057
|
} catch (err) {
|
|
5317
|
-
|
|
5058
|
+
logger23.warn("Failed to cleanup worktree", { dir: wtCtx.gitRootDir, error: err.message });
|
|
5318
5059
|
}
|
|
5319
5060
|
}
|
|
5320
5061
|
async installDependencies(workDir) {
|
|
5321
|
-
|
|
5062
|
+
logger23.info("Installing dependencies in worktree", { workDir });
|
|
5322
5063
|
const knowledge = getProjectKnowledge() ?? KNOWLEDGE_DEFAULTS;
|
|
5323
5064
|
const pkgMgr = knowledge.toolchain.packageManager.toLowerCase();
|
|
5324
5065
|
const isNodeProject = ["npm", "pnpm", "yarn", "bun"].some((m) => pkgMgr.includes(m));
|
|
5325
5066
|
if (isNodeProject) {
|
|
5326
5067
|
const ready = await this.ensureNodeModules(workDir);
|
|
5327
5068
|
if (ready) {
|
|
5328
|
-
|
|
5069
|
+
logger23.info("node_modules ready \u2014 skipping install");
|
|
5329
5070
|
return;
|
|
5330
5071
|
}
|
|
5331
5072
|
}
|
|
@@ -5338,10 +5079,10 @@ var PipelineOrchestrator = class {
|
|
|
5338
5079
|
maxBuffer: 10 * 1024 * 1024,
|
|
5339
5080
|
timeout: 3e5
|
|
5340
5081
|
});
|
|
5341
|
-
|
|
5082
|
+
logger23.info("Dependencies installed");
|
|
5342
5083
|
} catch (err) {
|
|
5343
5084
|
if (fallbackCmd) {
|
|
5344
|
-
|
|
5085
|
+
logger23.warn(`${installCmd} failed, retrying with fallback command`, {
|
|
5345
5086
|
error: err.message
|
|
5346
5087
|
});
|
|
5347
5088
|
const [fallbackBin, ...fallbackArgs] = fallbackCmd.split(/\s+/);
|
|
@@ -5351,45 +5092,45 @@ var PipelineOrchestrator = class {
|
|
|
5351
5092
|
maxBuffer: 10 * 1024 * 1024,
|
|
5352
5093
|
timeout: 3e5
|
|
5353
5094
|
});
|
|
5354
|
-
|
|
5095
|
+
logger23.info("Dependencies installed (fallback)");
|
|
5355
5096
|
} catch (retryErr) {
|
|
5356
|
-
|
|
5097
|
+
logger23.warn("Fallback install also failed", {
|
|
5357
5098
|
error: retryErr.message
|
|
5358
5099
|
});
|
|
5359
5100
|
}
|
|
5360
5101
|
} else {
|
|
5361
|
-
|
|
5102
|
+
logger23.warn("Install failed, no fallback configured", {
|
|
5362
5103
|
error: err.message
|
|
5363
5104
|
});
|
|
5364
5105
|
}
|
|
5365
5106
|
}
|
|
5366
5107
|
}
|
|
5367
5108
|
async ensureNodeModules(workDir) {
|
|
5368
|
-
const targetBin =
|
|
5109
|
+
const targetBin = path11.join(workDir, "node_modules", ".bin");
|
|
5369
5110
|
try {
|
|
5370
|
-
await
|
|
5371
|
-
|
|
5111
|
+
await fs9.access(targetBin);
|
|
5112
|
+
logger23.info("node_modules already complete (has .bin/)");
|
|
5372
5113
|
return true;
|
|
5373
5114
|
} catch {
|
|
5374
5115
|
}
|
|
5375
|
-
const sourceNM =
|
|
5376
|
-
const targetNM =
|
|
5116
|
+
const sourceNM = path11.join(this.config.project.workDir, "node_modules");
|
|
5117
|
+
const targetNM = path11.join(workDir, "node_modules");
|
|
5377
5118
|
try {
|
|
5378
|
-
await
|
|
5119
|
+
await fs9.access(sourceNM);
|
|
5379
5120
|
} catch {
|
|
5380
|
-
|
|
5121
|
+
logger23.warn("Main repo node_modules not found, skipping seed", { sourceNM });
|
|
5381
5122
|
return false;
|
|
5382
5123
|
}
|
|
5383
|
-
|
|
5124
|
+
logger23.info("Seeding node_modules from main repo via reflink copy", { sourceNM, targetNM });
|
|
5384
5125
|
try {
|
|
5385
5126
|
await execFileAsync2("rm", ["-rf", targetNM], { timeout: 6e4 });
|
|
5386
5127
|
await execFileAsync2("cp", ["-a", "--reflink=auto", sourceNM, targetNM], {
|
|
5387
5128
|
timeout: 12e4
|
|
5388
5129
|
});
|
|
5389
|
-
|
|
5130
|
+
logger23.info("node_modules seeded from main repo");
|
|
5390
5131
|
return true;
|
|
5391
5132
|
} catch (err) {
|
|
5392
|
-
|
|
5133
|
+
logger23.warn("Failed to seed node_modules from main repo", {
|
|
5393
5134
|
error: err.message
|
|
5394
5135
|
});
|
|
5395
5136
|
return false;
|
|
@@ -5399,16 +5140,16 @@ var PipelineOrchestrator = class {
|
|
|
5399
5140
|
const record = this.tracker.get(issueIid);
|
|
5400
5141
|
if (!record) throw new IssueNotFoundError(issueIid);
|
|
5401
5142
|
const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
|
|
5402
|
-
|
|
5143
|
+
logger23.info("Restarting issue \u2014 cleaning context", { issueIid, branchName: record.branchName });
|
|
5403
5144
|
this.pendingActions.set(issueIid, "restart");
|
|
5404
5145
|
this.aiRunner.killByWorkDir(wtCtx.workDir);
|
|
5405
5146
|
this.e2eAiRunner?.killByWorkDir(wtCtx.workDir);
|
|
5406
5147
|
this.stopPreviewServers(issueIid);
|
|
5407
5148
|
try {
|
|
5408
5149
|
const deleted = await this.gongfeng.cleanupAgentNotes(getExternalId(record));
|
|
5409
|
-
|
|
5150
|
+
logger23.info("Agent notes cleaned up", { issueIid, deleted });
|
|
5410
5151
|
} catch (err) {
|
|
5411
|
-
|
|
5152
|
+
logger23.warn("Failed to cleanup agent notes", { issueIid, error: err.message });
|
|
5412
5153
|
}
|
|
5413
5154
|
await this.mainGitMutex.runExclusive(async () => {
|
|
5414
5155
|
await this.cleanupWorktree(wtCtx);
|
|
@@ -5425,19 +5166,19 @@ var PipelineOrchestrator = class {
|
|
|
5425
5166
|
await this.cleanupE2eOutputs(issueIid);
|
|
5426
5167
|
this.tracker.resetFull(issueIid);
|
|
5427
5168
|
this.pendingActions.delete(issueIid);
|
|
5428
|
-
|
|
5169
|
+
logger23.info("Issue restarted", { issueIid });
|
|
5429
5170
|
}
|
|
5430
5171
|
async cancelIssue(issueIid) {
|
|
5431
5172
|
const record = this.tracker.get(issueIid);
|
|
5432
5173
|
if (!record) throw new IssueNotFoundError(issueIid);
|
|
5433
5174
|
const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
|
|
5434
|
-
|
|
5175
|
+
logger23.info("Cancelling issue \u2014 cleaning all resources", { issueIid, branchName: record.branchName });
|
|
5435
5176
|
this.aiRunner.killByWorkDir(wtCtx.workDir);
|
|
5436
5177
|
this.stopPreviewServers(issueIid);
|
|
5437
5178
|
try {
|
|
5438
5179
|
await this.gongfeng.removeLabelsWithPrefix(getExternalId(record), "auto-finish");
|
|
5439
5180
|
} catch (err) {
|
|
5440
|
-
|
|
5181
|
+
logger23.warn("Failed to remove labels on cancel", { issueIid, error: err.message });
|
|
5441
5182
|
}
|
|
5442
5183
|
await this.mainGitMutex.runExclusive(async () => {
|
|
5443
5184
|
await this.cleanupWorktree(wtCtx);
|
|
@@ -5454,7 +5195,7 @@ var PipelineOrchestrator = class {
|
|
|
5454
5195
|
this.tracker.clearProcessingLock(issueIid);
|
|
5455
5196
|
this.tracker.updateState(issueIid, "skipped" /* Skipped */);
|
|
5456
5197
|
await this.cleanupE2eOutputs(issueIid);
|
|
5457
|
-
|
|
5198
|
+
logger23.info("Issue cancelled", { issueIid });
|
|
5458
5199
|
}
|
|
5459
5200
|
/**
|
|
5460
5201
|
* Remove the E2E output directory for an issue: {uatVendorDir}/outputs/issue-{iid}
|
|
@@ -5462,13 +5203,13 @@ var PipelineOrchestrator = class {
|
|
|
5462
5203
|
async cleanupE2eOutputs(issueIid) {
|
|
5463
5204
|
const vendorDir = this.config.e2e.uatVendorDir;
|
|
5464
5205
|
if (!vendorDir) return;
|
|
5465
|
-
const abs =
|
|
5466
|
-
const outputDir =
|
|
5206
|
+
const abs = path11.isAbsolute(vendorDir) ? vendorDir : path11.resolve(this.config.project.workDir, vendorDir);
|
|
5207
|
+
const outputDir = path11.join(abs, "outputs", `issue-${issueIid}`);
|
|
5467
5208
|
try {
|
|
5468
|
-
await
|
|
5469
|
-
|
|
5209
|
+
await fs9.rm(outputDir, { recursive: true, force: true });
|
|
5210
|
+
logger23.info("E2E outputs cleaned up", { issueIid, dir: outputDir });
|
|
5470
5211
|
} catch (err) {
|
|
5471
|
-
|
|
5212
|
+
logger23.warn("Failed to cleanup E2E outputs", { issueIid, dir: outputDir, error: err.message });
|
|
5472
5213
|
}
|
|
5473
5214
|
}
|
|
5474
5215
|
/**
|
|
@@ -5480,10 +5221,10 @@ var PipelineOrchestrator = class {
|
|
|
5480
5221
|
if (!this.workspaceManager) return;
|
|
5481
5222
|
const wsRoot = this.workspaceManager.getWorkspaceRoot(issueIid);
|
|
5482
5223
|
try {
|
|
5483
|
-
await
|
|
5484
|
-
|
|
5224
|
+
await fs9.rm(wsRoot, { recursive: true, force: true });
|
|
5225
|
+
logger23.info("Workspace root cleaned up", { issueIid, dir: wsRoot });
|
|
5485
5226
|
} catch (err) {
|
|
5486
|
-
|
|
5227
|
+
logger23.warn("Failed to cleanup workspace root", { issueIid, dir: wsRoot, error: err.message });
|
|
5487
5228
|
}
|
|
5488
5229
|
}
|
|
5489
5230
|
retryFromPhase(issueIid, phase) {
|
|
@@ -5499,7 +5240,7 @@ var PipelineOrchestrator = class {
|
|
|
5499
5240
|
this.aiRunner.killByWorkDir(wtCtx.workDir);
|
|
5500
5241
|
}
|
|
5501
5242
|
this.e2eAiRunner?.killByWorkDir(wtCtx.workDir);
|
|
5502
|
-
|
|
5243
|
+
logger23.info("Retrying issue from phase", { issueIid, phase });
|
|
5503
5244
|
const ok = this.tracker.resetToPhase(issueIid, phase, issueDef);
|
|
5504
5245
|
if (!ok) {
|
|
5505
5246
|
throw new InvalidPhaseError(phase);
|
|
@@ -5526,7 +5267,7 @@ var PipelineOrchestrator = class {
|
|
|
5526
5267
|
} else {
|
|
5527
5268
|
this.tracker.pauseIssue(issueIid, record.currentPhase ?? "");
|
|
5528
5269
|
}
|
|
5529
|
-
|
|
5270
|
+
logger23.info("Issue abort requested", { issueIid, state: record.state });
|
|
5530
5271
|
}
|
|
5531
5272
|
continueIssue(issueIid) {
|
|
5532
5273
|
const record = this.tracker.get(issueIid);
|
|
@@ -5536,7 +5277,7 @@ var PipelineOrchestrator = class {
|
|
|
5536
5277
|
}
|
|
5537
5278
|
const issueDef = this.getIssueSpecificPipelineDef(record);
|
|
5538
5279
|
this.tracker.resumeFromPause(issueIid, issueDef, false);
|
|
5539
|
-
|
|
5280
|
+
logger23.info("Issue continued from pause", { issueIid });
|
|
5540
5281
|
}
|
|
5541
5282
|
redoPhase(issueIid) {
|
|
5542
5283
|
const record = this.tracker.get(issueIid);
|
|
@@ -5580,7 +5321,7 @@ var PipelineOrchestrator = class {
|
|
|
5580
5321
|
}
|
|
5581
5322
|
this.eventBus.emitTyped("issue:redone", { issueIid });
|
|
5582
5323
|
}
|
|
5583
|
-
|
|
5324
|
+
logger23.info("Issue redo requested", { issueIid, state: record.state });
|
|
5584
5325
|
}
|
|
5585
5326
|
/**
|
|
5586
5327
|
* 处理中止/重做的共享逻辑:
|
|
@@ -5653,7 +5394,7 @@ var PipelineOrchestrator = class {
|
|
|
5653
5394
|
async _processIssueImpl(issue) {
|
|
5654
5395
|
const branchName = `${this.config.project.branchPrefix}-${issue.iid}`;
|
|
5655
5396
|
const wtCtx = this.computeWorktreeContext(issue.iid, branchName);
|
|
5656
|
-
|
|
5397
|
+
logger23.info("Processing issue", {
|
|
5657
5398
|
iid: issue.iid,
|
|
5658
5399
|
title: issue.title,
|
|
5659
5400
|
branchName,
|
|
@@ -5760,7 +5501,7 @@ var PipelineOrchestrator = class {
|
|
|
5760
5501
|
title,
|
|
5761
5502
|
description
|
|
5762
5503
|
});
|
|
5763
|
-
|
|
5504
|
+
logger23.info("Merge request created successfully", {
|
|
5764
5505
|
iid: issue.iid,
|
|
5765
5506
|
mrIid: mr.iid,
|
|
5766
5507
|
mrUrl: mr.web_url
|
|
@@ -5768,7 +5509,7 @@ var PipelineOrchestrator = class {
|
|
|
5768
5509
|
return { url: mr.web_url, iid: mr.iid };
|
|
5769
5510
|
} catch (err) {
|
|
5770
5511
|
const errorMsg = err.message;
|
|
5771
|
-
|
|
5512
|
+
logger23.warn("Failed to create merge request, trying to find existing one", {
|
|
5772
5513
|
iid: issue.iid,
|
|
5773
5514
|
error: errorMsg
|
|
5774
5515
|
});
|
|
@@ -5785,7 +5526,7 @@ var PipelineOrchestrator = class {
|
|
|
5785
5526
|
this.config.project.baseBranch
|
|
5786
5527
|
);
|
|
5787
5528
|
if (existing) {
|
|
5788
|
-
|
|
5529
|
+
logger23.info("Found existing merge request", {
|
|
5789
5530
|
iid: issueIid,
|
|
5790
5531
|
mrIid: existing.iid,
|
|
5791
5532
|
mrUrl: existing.web_url
|
|
@@ -5793,7 +5534,7 @@ var PipelineOrchestrator = class {
|
|
|
5793
5534
|
return { url: existing.web_url, iid: existing.iid };
|
|
5794
5535
|
}
|
|
5795
5536
|
} catch (findErr) {
|
|
5796
|
-
|
|
5537
|
+
logger23.warn("Failed to find existing merge request", {
|
|
5797
5538
|
iid: issueIid,
|
|
5798
5539
|
error: findErr.message
|
|
5799
5540
|
});
|
|
@@ -5838,7 +5579,7 @@ var PipelineOrchestrator = class {
|
|
|
5838
5579
|
});
|
|
5839
5580
|
return ports;
|
|
5840
5581
|
} catch (err) {
|
|
5841
|
-
|
|
5582
|
+
logger23.error("Failed to start preview servers", {
|
|
5842
5583
|
iid: issue.iid,
|
|
5843
5584
|
error: err.message
|
|
5844
5585
|
});
|
|
@@ -5873,7 +5614,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5873
5614
|
await this.mainGitMutex.runExclusive(async () => {
|
|
5874
5615
|
await this.cleanupWorktree(wtCtx);
|
|
5875
5616
|
});
|
|
5876
|
-
|
|
5617
|
+
logger23.info("Preview stopped and worktree cleaned", { iid: issueIid });
|
|
5877
5618
|
}
|
|
5878
5619
|
async markDeployed(issueIid) {
|
|
5879
5620
|
const record = this.tracker.get(issueIid);
|
|
@@ -5890,7 +5631,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5890
5631
|
try {
|
|
5891
5632
|
await this.gongfeng.closeIssue(externalId);
|
|
5892
5633
|
} catch (err) {
|
|
5893
|
-
|
|
5634
|
+
logger23.warn("Failed to close issue on Gongfeng", { iid: issueIid, error: err.message });
|
|
5894
5635
|
}
|
|
5895
5636
|
try {
|
|
5896
5637
|
const issue = await this.gongfeng.getIssueDetail(externalId);
|
|
@@ -5898,10 +5639,10 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5898
5639
|
labels.push("auto-finish:deployed");
|
|
5899
5640
|
await this.gongfeng.updateIssueLabels(externalId, labels);
|
|
5900
5641
|
} catch (err) {
|
|
5901
|
-
|
|
5642
|
+
logger23.warn("Failed to update labels", { iid: issueIid, error: err.message });
|
|
5902
5643
|
}
|
|
5903
5644
|
this.tracker.updateState(issueIid, "deployed" /* Deployed */);
|
|
5904
|
-
|
|
5645
|
+
logger23.info("Issue marked as deployed", { iid: issueIid });
|
|
5905
5646
|
}
|
|
5906
5647
|
async restartPreview(issueIid) {
|
|
5907
5648
|
const record = this.tracker.get(issueIid);
|
|
@@ -5928,7 +5669,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5928
5669
|
throw err;
|
|
5929
5670
|
}
|
|
5930
5671
|
const url = this.buildPreviewUrl(issueIid);
|
|
5931
|
-
|
|
5672
|
+
logger23.info("Preview restarted", { iid: issueIid, url });
|
|
5932
5673
|
return url;
|
|
5933
5674
|
}
|
|
5934
5675
|
getPreviewHost() {
|
|
@@ -5961,7 +5702,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5961
5702
|
if (!record) throw new IssueNotFoundError(issueIid);
|
|
5962
5703
|
const baseBranch = this.config.project.baseBranch;
|
|
5963
5704
|
const branchName = record.branchName;
|
|
5964
|
-
|
|
5705
|
+
logger23.info("Starting conflict resolution", { issueIid, branchName, baseBranch });
|
|
5965
5706
|
this.tracker.updateState(issueIid, "resolving_conflict" /* ResolvingConflict */);
|
|
5966
5707
|
this.eventBus.emitTyped("conflict:started", { issueIid });
|
|
5967
5708
|
try {
|
|
@@ -5994,7 +5735,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5994
5735
|
});
|
|
5995
5736
|
}
|
|
5996
5737
|
});
|
|
5997
|
-
|
|
5738
|
+
logger23.info("Running verification after conflict resolution", { issueIid });
|
|
5998
5739
|
const wtPlan = new PlanPersistence(wtCtx.workDir, issueIid);
|
|
5999
5740
|
wtPlan.ensureDir();
|
|
6000
5741
|
const verifyPhase = createPhase("verify", this.aiRunner, wtGit, wtPlan, this.config);
|
|
@@ -6032,10 +5773,10 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
6032
5773
|
} catch {
|
|
6033
5774
|
}
|
|
6034
5775
|
await this.commentOnMr(record.mrUrl, t("conflict.mrResolvedComment"));
|
|
6035
|
-
|
|
5776
|
+
logger23.info("Conflict resolution completed", { issueIid });
|
|
6036
5777
|
} catch (err) {
|
|
6037
5778
|
const errorMsg = err.message;
|
|
6038
|
-
|
|
5779
|
+
logger23.error("Conflict resolution failed", { issueIid, error: errorMsg });
|
|
6039
5780
|
try {
|
|
6040
5781
|
const wtGit = new GitOperations(wtCtx.gitRootDir);
|
|
6041
5782
|
if (await wtGit.isRebaseInProgress()) {
|
|
@@ -6065,7 +5806,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
6065
5806
|
try {
|
|
6066
5807
|
await this.gongfeng.createMergeRequestNote(mrIid, body);
|
|
6067
5808
|
} catch (err) {
|
|
6068
|
-
|
|
5809
|
+
logger23.warn("Failed to comment on MR", { mrIid, error: err.message });
|
|
6069
5810
|
}
|
|
6070
5811
|
}
|
|
6071
5812
|
};
|
|
@@ -6141,7 +5882,7 @@ ${questions}
|
|
|
6141
5882
|
}
|
|
6142
5883
|
|
|
6143
5884
|
// src/services/BrainstormService.ts
|
|
6144
|
-
var
|
|
5885
|
+
var logger24 = logger.child("Brainstorm");
|
|
6145
5886
|
function agentConfigToAIConfig(agentCfg, timeoutMs) {
|
|
6146
5887
|
return {
|
|
6147
5888
|
mode: agentCfg.mode,
|
|
@@ -6177,7 +5918,7 @@ var BrainstormService = class {
|
|
|
6177
5918
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6178
5919
|
};
|
|
6179
5920
|
this.sessions.set(session.id, session);
|
|
6180
|
-
|
|
5921
|
+
logger24.info("Created brainstorm session", { sessionId: session.id });
|
|
6181
5922
|
return session;
|
|
6182
5923
|
}
|
|
6183
5924
|
getSession(id) {
|
|
@@ -6186,7 +5927,7 @@ var BrainstormService = class {
|
|
|
6186
5927
|
async generate(sessionId, onEvent) {
|
|
6187
5928
|
const session = this.requireSession(sessionId);
|
|
6188
5929
|
session.status = "generating";
|
|
6189
|
-
|
|
5930
|
+
logger24.info("Generating SDD", { sessionId });
|
|
6190
5931
|
const prompt = buildGeneratePrompt(session.transcript);
|
|
6191
5932
|
const result = await this.generatorRunner.run({
|
|
6192
5933
|
prompt,
|
|
@@ -6212,7 +5953,7 @@ var BrainstormService = class {
|
|
|
6212
5953
|
const session = this.requireSession(sessionId);
|
|
6213
5954
|
const roundNum = session.rounds.length + 1;
|
|
6214
5955
|
session.status = "reviewing";
|
|
6215
|
-
|
|
5956
|
+
logger24.info("Reviewing SDD", { sessionId, round: roundNum });
|
|
6216
5957
|
onEvent?.({ type: "round:start", data: { round: roundNum, phase: "review" }, round: roundNum });
|
|
6217
5958
|
const prompt = buildReviewPrompt(session.currentSdd, roundNum);
|
|
6218
5959
|
const result = await this.reviewerRunner.run({
|
|
@@ -6245,7 +5986,7 @@ var BrainstormService = class {
|
|
|
6245
5986
|
throw new Error("No review round to refine from");
|
|
6246
5987
|
}
|
|
6247
5988
|
session.status = "refining";
|
|
6248
|
-
|
|
5989
|
+
logger24.info("Refining SDD", { sessionId, round: currentRound.round });
|
|
6249
5990
|
const prompt = buildRefinePrompt(currentRound.questions);
|
|
6250
5991
|
const result = await this.generatorRunner.run({
|
|
6251
5992
|
prompt,
|
|
@@ -6332,4 +6073,4 @@ export {
|
|
|
6332
6073
|
PipelineOrchestrator,
|
|
6333
6074
|
BrainstormService
|
|
6334
6075
|
};
|
|
6335
|
-
//# sourceMappingURL=chunk-
|
|
6076
|
+
//# sourceMappingURL=chunk-OPWP73PW.js.map
|