@xdevops/issue-auto-finish 1.0.90 → 1.0.92
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/{chunk-TDKTI363.js → chunk-2WDVTLVF.js} +1 -1
- package/dist/{chunk-Y62E72TA.js → chunk-6T7ZHAV2.js} +2 -2
- package/dist/{chunk-IWSMQXBL.js → chunk-WZGEYHCC.js} +1300 -600
- package/dist/chunk-WZGEYHCC.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/hooks/HookEventWatcher.d.ts +34 -0
- package/dist/hooks/HookEventWatcher.d.ts.map +1 -0
- package/dist/hooks/HookInjector.d.ts +85 -0
- package/dist/hooks/HookInjector.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/lib.js +1 -1
- 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/steps/SetupStep.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/phases/BasePhase.d.ts +2 -0
- package/dist/phases/BasePhase.d.ts.map +1 -1
- package/dist/{restart-2BCP6AMK.js → restart-5D3ZDD5L.js} +2 -2
- package/dist/run.js +2 -2
- package/dist/{start-ECUOKGM2.js → start-IQBNXLEI.js} +2 -2
- package/package.json +1 -1
- package/src/web/frontend/dist/assets/{index-Dby4j-V_.js → index-BR0UoQER.js} +2 -2
- package/src/web/frontend/dist/index.html +1 -1
- package/dist/chunk-IWSMQXBL.js.map +0 -1
- /package/dist/{chunk-TDKTI363.js.map → chunk-2WDVTLVF.js.map} +0 -0
- /package/dist/{chunk-Y62E72TA.js.map → chunk-6T7ZHAV2.js.map} +0 -0
- /package/dist/{restart-2BCP6AMK.js.map → restart-5D3ZDD5L.js.map} +0 -0
- /package/dist/{start-ECUOKGM2.js.map → start-IQBNXLEI.js.map} +0 -0
|
@@ -233,8 +233,8 @@ var GongfengClient = class {
|
|
|
233
233
|
const encoded = encodeURIComponent(this.projectPath);
|
|
234
234
|
return `${this.apiUrl}/api/v3/projects/${encoded}`;
|
|
235
235
|
}
|
|
236
|
-
async requestRaw(
|
|
237
|
-
const url = `${this.projectApiBase}${
|
|
236
|
+
async requestRaw(path13, options = {}) {
|
|
237
|
+
const url = `${this.projectApiBase}${path13}`;
|
|
238
238
|
logger4.debug("API request", { method: options.method || "GET", url });
|
|
239
239
|
return this.circuitBreaker.execute(
|
|
240
240
|
() => this.retryPolicy.execute(async () => {
|
|
@@ -251,11 +251,11 @@ var GongfengClient = class {
|
|
|
251
251
|
throw new GongfengApiError(resp.status, `Gongfeng API error ${resp.status}: ${body}`, body);
|
|
252
252
|
}
|
|
253
253
|
return resp;
|
|
254
|
-
}, `requestRaw ${options.method || "GET"} ${
|
|
254
|
+
}, `requestRaw ${options.method || "GET"} ${path13}`)
|
|
255
255
|
);
|
|
256
256
|
}
|
|
257
|
-
async request(
|
|
258
|
-
const resp = await this.requestRaw(
|
|
257
|
+
async request(path13, options = {}) {
|
|
258
|
+
const resp = await this.requestRaw(path13, options);
|
|
259
259
|
return resp.json();
|
|
260
260
|
}
|
|
261
261
|
async createIssue(title, description, labels) {
|
|
@@ -434,8 +434,8 @@ var GongfengClient = class {
|
|
|
434
434
|
}
|
|
435
435
|
return mr;
|
|
436
436
|
}
|
|
437
|
-
async requestGlobal(
|
|
438
|
-
const url = `${this.apiUrl}${
|
|
437
|
+
async requestGlobal(path13, options = {}) {
|
|
438
|
+
const url = `${this.apiUrl}${path13}`;
|
|
439
439
|
logger4.debug("API request (global)", { method: options.method || "GET", url });
|
|
440
440
|
const resp = await this.circuitBreaker.execute(
|
|
441
441
|
() => this.retryPolicy.execute(async () => {
|
|
@@ -452,7 +452,7 @@ var GongfengClient = class {
|
|
|
452
452
|
throw new GongfengApiError(r.status, `Gongfeng API error ${r.status}: ${body}`, body);
|
|
453
453
|
}
|
|
454
454
|
return r;
|
|
455
|
-
}, `requestGlobal ${options.method || "GET"} ${
|
|
455
|
+
}, `requestGlobal ${options.method || "GET"} ${path13}`)
|
|
456
456
|
);
|
|
457
457
|
return resp.json();
|
|
458
458
|
}
|
|
@@ -1646,7 +1646,7 @@ var PlanPersistence = class _PlanPersistence {
|
|
|
1646
1646
|
};
|
|
1647
1647
|
|
|
1648
1648
|
// src/phases/BasePhase.ts
|
|
1649
|
-
import
|
|
1649
|
+
import path5 from "path";
|
|
1650
1650
|
|
|
1651
1651
|
// src/rules/RuleResolver.ts
|
|
1652
1652
|
import { readdir, readFile } from "fs/promises";
|
|
@@ -1743,6 +1743,454 @@ ${rule.content}`;
|
|
|
1743
1743
|
}
|
|
1744
1744
|
};
|
|
1745
1745
|
|
|
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
|
+
|
|
1746
2194
|
// src/phases/BasePhase.ts
|
|
1747
2195
|
var BasePhase = class _BasePhase {
|
|
1748
2196
|
static MIN_ARTIFACT_BYTES = 50;
|
|
@@ -1887,7 +2335,7 @@ ${t("basePhase.rulesSection", { rules })}`;
|
|
|
1887
2335
|
const resultFiles = this.getResultFiles(ctx);
|
|
1888
2336
|
const snapshotFilenames = resultFiles.map((f) => f.filename);
|
|
1889
2337
|
const artifactCheck = snapshotFilenames.length > 0 ? () => snapshotFilenames.every((fn) => this.plan.isArtifactReady(fn, _BasePhase.MIN_ARTIFACT_BYTES)) : void 0;
|
|
1890
|
-
const artifactPaths = snapshotFilenames.length > 0 ? snapshotFilenames.map((fn) =>
|
|
2338
|
+
const artifactPaths = snapshotFilenames.length > 0 ? snapshotFilenames.map((fn) => path5.join(this.plan.planDir, fn)) : void 0;
|
|
1891
2339
|
let capturedSessionId;
|
|
1892
2340
|
const result = await this.aiRunner.run({
|
|
1893
2341
|
prompt,
|
|
@@ -1984,14 +2432,14 @@ ${t("basePhase.rulesSection", { rules })}`;
|
|
|
1984
2432
|
const context = `${ctx.demand.title} ${ctx.demand.description} ${ctx.demand.supplement ? JSON.stringify(ctx.demand.supplement) : ""}`;
|
|
1985
2433
|
if (ctx.workspace && ctx.workspace.repos.length > 1) {
|
|
1986
2434
|
for (const repo of ctx.workspace.repos) {
|
|
1987
|
-
const rulesDir =
|
|
2435
|
+
const rulesDir = path5.join(repo.gitRootDir, ".cursor", "rules");
|
|
1988
2436
|
try {
|
|
1989
2437
|
await resolver.loadRules(rulesDir);
|
|
1990
2438
|
} catch {
|
|
1991
2439
|
}
|
|
1992
2440
|
}
|
|
1993
2441
|
} else {
|
|
1994
|
-
const rulesDir =
|
|
2442
|
+
const rulesDir = path5.join(this.plan.baseDir, ".cursor", "rules");
|
|
1995
2443
|
await resolver.loadRules(rulesDir);
|
|
1996
2444
|
}
|
|
1997
2445
|
const matched = resolver.matchRules(context);
|
|
@@ -2024,6 +2472,24 @@ ${t("basePhase.rulesSection", { rules })}`;
|
|
|
2024
2472
|
this.logger.error(msg, { phase: this.phaseName, displayId: _displayId });
|
|
2025
2473
|
throw new AIExecutionError(this.phaseName, msg, { output: "", exitCode: 0 });
|
|
2026
2474
|
}
|
|
2475
|
+
this.logHookManifest();
|
|
2476
|
+
}
|
|
2477
|
+
/** 读取 hook manifest 并记录产物写入历史(P1 增强层,不影响主流程) */
|
|
2478
|
+
logHookManifest() {
|
|
2479
|
+
try {
|
|
2480
|
+
const injector = new HookInjector();
|
|
2481
|
+
const entries = injector.readManifest(this.plan.baseDir);
|
|
2482
|
+
if (entries.length === 0) return;
|
|
2483
|
+
const artifactWrites = entries.filter((e) => "file" in e && e.event === "write");
|
|
2484
|
+
if (artifactWrites.length > 0) {
|
|
2485
|
+
this.logger.info("Hook manifest: artifact writes recorded", {
|
|
2486
|
+
phase: this.phaseName,
|
|
2487
|
+
writes: artifactWrites.length,
|
|
2488
|
+
files: artifactWrites.map((e) => "file" in e ? e.file : "unknown")
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
} catch {
|
|
2492
|
+
}
|
|
2027
2493
|
}
|
|
2028
2494
|
};
|
|
2029
2495
|
|
|
@@ -2247,20 +2713,20 @@ var BuildPhase = class extends BasePhase {
|
|
|
2247
2713
|
};
|
|
2248
2714
|
|
|
2249
2715
|
// src/release/ReleaseDetectCache.ts
|
|
2250
|
-
import
|
|
2251
|
-
import
|
|
2716
|
+
import fs5 from "fs";
|
|
2717
|
+
import path6 from "path";
|
|
2252
2718
|
import { createHash } from "crypto";
|
|
2253
|
-
var
|
|
2719
|
+
var logger9 = logger.child("ReleaseDetectCache");
|
|
2254
2720
|
function hashProjectPath(projectPath) {
|
|
2255
2721
|
return createHash("sha256").update(projectPath).digest("hex").slice(0, 16);
|
|
2256
2722
|
}
|
|
2257
2723
|
var ReleaseDetectCache = class {
|
|
2258
2724
|
cacheDir;
|
|
2259
2725
|
constructor(dataDir) {
|
|
2260
|
-
this.cacheDir =
|
|
2726
|
+
this.cacheDir = path6.join(dataDir, "release-detect");
|
|
2261
2727
|
}
|
|
2262
2728
|
filePath(projectPath) {
|
|
2263
|
-
return
|
|
2729
|
+
return path6.join(this.cacheDir, `${hashProjectPath(projectPath)}.json`);
|
|
2264
2730
|
}
|
|
2265
2731
|
/**
|
|
2266
2732
|
* 读取缓存。返回 null 如果不存在、已过期或校验失败。
|
|
@@ -2268,21 +2734,21 @@ var ReleaseDetectCache = class {
|
|
|
2268
2734
|
get(projectPath, ttlMs) {
|
|
2269
2735
|
const fp = this.filePath(projectPath);
|
|
2270
2736
|
try {
|
|
2271
|
-
if (!
|
|
2272
|
-
const raw =
|
|
2737
|
+
if (!fs5.existsSync(fp)) return null;
|
|
2738
|
+
const raw = fs5.readFileSync(fp, "utf-8");
|
|
2273
2739
|
const data = JSON.parse(raw);
|
|
2274
2740
|
if (data.projectPath !== projectPath) {
|
|
2275
|
-
|
|
2741
|
+
logger9.warn("Cache projectPath mismatch, ignoring", { expected: projectPath, got: data.projectPath });
|
|
2276
2742
|
return null;
|
|
2277
2743
|
}
|
|
2278
2744
|
const age = Date.now() - new Date(data.detectedAt).getTime();
|
|
2279
2745
|
if (age > ttlMs) {
|
|
2280
|
-
|
|
2746
|
+
logger9.debug("Cache expired", { projectPath, ageMs: age, ttlMs });
|
|
2281
2747
|
return null;
|
|
2282
2748
|
}
|
|
2283
2749
|
return data;
|
|
2284
2750
|
} catch (err) {
|
|
2285
|
-
|
|
2751
|
+
logger9.warn("Failed to read release detect cache", { path: fp, error: err.message });
|
|
2286
2752
|
return null;
|
|
2287
2753
|
}
|
|
2288
2754
|
}
|
|
@@ -2292,13 +2758,13 @@ var ReleaseDetectCache = class {
|
|
|
2292
2758
|
set(result) {
|
|
2293
2759
|
const fp = this.filePath(result.projectPath);
|
|
2294
2760
|
try {
|
|
2295
|
-
if (!
|
|
2296
|
-
|
|
2761
|
+
if (!fs5.existsSync(this.cacheDir)) {
|
|
2762
|
+
fs5.mkdirSync(this.cacheDir, { recursive: true });
|
|
2297
2763
|
}
|
|
2298
|
-
|
|
2299
|
-
|
|
2764
|
+
fs5.writeFileSync(fp, JSON.stringify(result, null, 2), "utf-8");
|
|
2765
|
+
logger9.debug("Release detect cache written", { projectPath: result.projectPath, path: fp });
|
|
2300
2766
|
} catch (err) {
|
|
2301
|
-
|
|
2767
|
+
logger9.warn("Failed to write release detect cache", { path: fp, error: err.message });
|
|
2302
2768
|
}
|
|
2303
2769
|
}
|
|
2304
2770
|
/**
|
|
@@ -2307,14 +2773,14 @@ var ReleaseDetectCache = class {
|
|
|
2307
2773
|
invalidate(projectPath) {
|
|
2308
2774
|
const fp = this.filePath(projectPath);
|
|
2309
2775
|
try {
|
|
2310
|
-
if (
|
|
2311
|
-
|
|
2312
|
-
|
|
2776
|
+
if (fs5.existsSync(fp)) {
|
|
2777
|
+
fs5.unlinkSync(fp);
|
|
2778
|
+
logger9.info("Release detect cache invalidated", { projectPath });
|
|
2313
2779
|
return true;
|
|
2314
2780
|
}
|
|
2315
2781
|
return false;
|
|
2316
2782
|
} catch (err) {
|
|
2317
|
-
|
|
2783
|
+
logger9.warn("Failed to invalidate release detect cache", { path: fp, error: err.message });
|
|
2318
2784
|
return false;
|
|
2319
2785
|
}
|
|
2320
2786
|
}
|
|
@@ -2765,9 +3231,9 @@ function createLifecycleManager(def) {
|
|
|
2765
3231
|
|
|
2766
3232
|
// src/workspace/WorkspaceConfig.ts
|
|
2767
3233
|
import { z } from "zod";
|
|
2768
|
-
import
|
|
2769
|
-
import
|
|
2770
|
-
var
|
|
3234
|
+
import fs6 from "fs";
|
|
3235
|
+
import path7 from "path";
|
|
3236
|
+
var logger10 = logger.child("WorkspaceConfig");
|
|
2771
3237
|
var repoConfigSchema = z.object({
|
|
2772
3238
|
name: z.string().min(1, "Repo name is required"),
|
|
2773
3239
|
projectPath: z.string().min(1, "Gongfeng project path is required"),
|
|
@@ -2783,29 +3249,29 @@ var workspaceConfigSchema = z.object({
|
|
|
2783
3249
|
});
|
|
2784
3250
|
function loadWorkspaceConfig(configPath) {
|
|
2785
3251
|
if (!configPath) return null;
|
|
2786
|
-
if (!
|
|
2787
|
-
|
|
3252
|
+
if (!fs6.existsSync(configPath)) {
|
|
3253
|
+
logger10.warn("Workspace config file not found, falling back to single-repo mode", {
|
|
2788
3254
|
path: configPath
|
|
2789
3255
|
});
|
|
2790
3256
|
return null;
|
|
2791
3257
|
}
|
|
2792
3258
|
try {
|
|
2793
|
-
const raw =
|
|
3259
|
+
const raw = fs6.readFileSync(configPath, "utf-8");
|
|
2794
3260
|
const json = JSON.parse(raw);
|
|
2795
3261
|
const result = workspaceConfigSchema.safeParse(json);
|
|
2796
3262
|
if (!result.success) {
|
|
2797
3263
|
const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
2798
|
-
|
|
3264
|
+
logger10.error(`Workspace config validation failed:
|
|
2799
3265
|
${issues}`);
|
|
2800
3266
|
return null;
|
|
2801
3267
|
}
|
|
2802
|
-
|
|
3268
|
+
logger10.info("Workspace config loaded", {
|
|
2803
3269
|
primary: result.data.primary.name,
|
|
2804
3270
|
associates: result.data.associates.map((a) => a.name)
|
|
2805
3271
|
});
|
|
2806
3272
|
return result.data;
|
|
2807
3273
|
} catch (err) {
|
|
2808
|
-
|
|
3274
|
+
logger10.error("Failed to parse workspace config", {
|
|
2809
3275
|
path: configPath,
|
|
2810
3276
|
error: err.message
|
|
2811
3277
|
});
|
|
@@ -2831,14 +3297,14 @@ function isMultiRepo(ws) {
|
|
|
2831
3297
|
}
|
|
2832
3298
|
function persistWorkspaceConfig(ws, filePath) {
|
|
2833
3299
|
try {
|
|
2834
|
-
const dir =
|
|
2835
|
-
if (!
|
|
2836
|
-
|
|
3300
|
+
const dir = path7.dirname(filePath);
|
|
3301
|
+
if (!fs6.existsSync(dir)) {
|
|
3302
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
2837
3303
|
}
|
|
2838
|
-
|
|
2839
|
-
|
|
3304
|
+
fs6.writeFileSync(filePath, JSON.stringify(ws, null, 2) + "\n", "utf-8");
|
|
3305
|
+
logger10.info("Workspace config auto-generated from .env", { path: filePath });
|
|
2840
3306
|
} catch (err) {
|
|
2841
|
-
|
|
3307
|
+
logger10.warn("Failed to persist workspace config", {
|
|
2842
3308
|
path: filePath,
|
|
2843
3309
|
error: err.message
|
|
2844
3310
|
});
|
|
@@ -2846,12 +3312,12 @@ function persistWorkspaceConfig(ws, filePath) {
|
|
|
2846
3312
|
}
|
|
2847
3313
|
|
|
2848
3314
|
// src/workspace/WorkspaceManager.ts
|
|
2849
|
-
import
|
|
2850
|
-
import
|
|
3315
|
+
import path8 from "path";
|
|
3316
|
+
import fs7 from "fs/promises";
|
|
2851
3317
|
import { execFile } from "child_process";
|
|
2852
3318
|
import { promisify } from "util";
|
|
2853
3319
|
var execFileAsync = promisify(execFile);
|
|
2854
|
-
var
|
|
3320
|
+
var logger11 = logger.child("WorkspaceManager");
|
|
2855
3321
|
var WorkspaceManager = class {
|
|
2856
3322
|
wsConfig;
|
|
2857
3323
|
worktreeBaseDir;
|
|
@@ -2880,7 +3346,7 @@ var WorkspaceManager = class {
|
|
|
2880
3346
|
*/
|
|
2881
3347
|
async prepareWorkspace(issueIid, branchName, globalBaseBranch, globalBranchPrefix) {
|
|
2882
3348
|
const wsRoot = this.getWorkspaceRoot(issueIid);
|
|
2883
|
-
await
|
|
3349
|
+
await fs7.mkdir(wsRoot, { recursive: true });
|
|
2884
3350
|
const primaryCtx = await this.preparePrimaryRepo(
|
|
2885
3351
|
issueIid,
|
|
2886
3352
|
branchName,
|
|
@@ -2899,7 +3365,7 @@ var WorkspaceManager = class {
|
|
|
2899
3365
|
);
|
|
2900
3366
|
associateCtxs.push(ctx);
|
|
2901
3367
|
}
|
|
2902
|
-
|
|
3368
|
+
logger11.info("Workspace prepared", {
|
|
2903
3369
|
issueIid,
|
|
2904
3370
|
wsRoot,
|
|
2905
3371
|
repos: [primaryCtx.name, ...associateCtxs.map((a) => a.name)]
|
|
@@ -2924,7 +3390,7 @@ var WorkspaceManager = class {
|
|
|
2924
3390
|
await git.commit(message);
|
|
2925
3391
|
await git.push(wsCtx.branchName);
|
|
2926
3392
|
committed.push(repo.name);
|
|
2927
|
-
|
|
3393
|
+
logger11.info("Committed and pushed changes", {
|
|
2928
3394
|
repo: repo.name,
|
|
2929
3395
|
branch: wsCtx.branchName
|
|
2930
3396
|
});
|
|
@@ -2938,19 +3404,19 @@ var WorkspaceManager = class {
|
|
|
2938
3404
|
async cleanupWorkspace(wsCtx) {
|
|
2939
3405
|
try {
|
|
2940
3406
|
await this.mainGit.worktreeRemove(wsCtx.primary.gitRootDir, true);
|
|
2941
|
-
|
|
3407
|
+
logger11.info("Primary worktree removed", { dir: wsCtx.primary.gitRootDir });
|
|
2942
3408
|
} catch (err) {
|
|
2943
|
-
|
|
3409
|
+
logger11.warn("Failed to remove primary worktree", {
|
|
2944
3410
|
dir: wsCtx.primary.gitRootDir,
|
|
2945
3411
|
error: err.message
|
|
2946
3412
|
});
|
|
2947
3413
|
}
|
|
2948
3414
|
for (const assoc of wsCtx.associates) {
|
|
2949
3415
|
try {
|
|
2950
|
-
await
|
|
2951
|
-
|
|
3416
|
+
await fs7.rm(assoc.gitRootDir, { recursive: true, force: true });
|
|
3417
|
+
logger11.info("Associate repo dir removed", { name: assoc.name, dir: assoc.gitRootDir });
|
|
2952
3418
|
} catch (err) {
|
|
2953
|
-
|
|
3419
|
+
logger11.warn("Failed to remove associate repo dir", {
|
|
2954
3420
|
name: assoc.name,
|
|
2955
3421
|
dir: assoc.gitRootDir,
|
|
2956
3422
|
error: err.message
|
|
@@ -2958,9 +3424,9 @@ var WorkspaceManager = class {
|
|
|
2958
3424
|
}
|
|
2959
3425
|
}
|
|
2960
3426
|
try {
|
|
2961
|
-
const entries = await
|
|
3427
|
+
const entries = await fs7.readdir(wsCtx.workspaceRoot);
|
|
2962
3428
|
if (entries.length === 0) {
|
|
2963
|
-
await
|
|
3429
|
+
await fs7.rmdir(wsCtx.workspaceRoot);
|
|
2964
3430
|
}
|
|
2965
3431
|
} catch {
|
|
2966
3432
|
}
|
|
@@ -2972,13 +3438,13 @@ var WorkspaceManager = class {
|
|
|
2972
3438
|
const wsRoot = this.getWorkspaceRoot(issueIid);
|
|
2973
3439
|
const primary = this.wsConfig.primary;
|
|
2974
3440
|
const defaultPrefix = globalBranchPrefix ?? primary.branchPrefix ?? "feat/issue";
|
|
2975
|
-
const primaryDir =
|
|
3441
|
+
const primaryDir = path8.join(wsRoot, primary.name);
|
|
2976
3442
|
const repos = [{
|
|
2977
3443
|
name: primary.name,
|
|
2978
3444
|
projectPath: primary.projectPath,
|
|
2979
3445
|
role: primary.role ?? "",
|
|
2980
3446
|
gitRootDir: primaryDir,
|
|
2981
|
-
workDir:
|
|
3447
|
+
workDir: path8.join(primaryDir, primary.projectSubDir ?? ""),
|
|
2982
3448
|
baseBranch: primary.baseBranch ?? globalBaseBranch,
|
|
2983
3449
|
branchPrefix: primary.branchPrefix ?? defaultPrefix,
|
|
2984
3450
|
isPrimary: true
|
|
@@ -2988,8 +3454,8 @@ var WorkspaceManager = class {
|
|
|
2988
3454
|
name: assoc.name,
|
|
2989
3455
|
projectPath: assoc.projectPath,
|
|
2990
3456
|
role: assoc.role ?? "",
|
|
2991
|
-
gitRootDir:
|
|
2992
|
-
workDir:
|
|
3457
|
+
gitRootDir: path8.join(wsRoot, assoc.name),
|
|
3458
|
+
workDir: path8.join(wsRoot, assoc.name, assoc.projectSubDir ?? ""),
|
|
2993
3459
|
baseBranch: assoc.baseBranch ?? globalBaseBranch,
|
|
2994
3460
|
branchPrefix: assoc.branchPrefix ?? defaultPrefix,
|
|
2995
3461
|
isPrimary: false
|
|
@@ -2998,12 +3464,12 @@ var WorkspaceManager = class {
|
|
|
2998
3464
|
return repos;
|
|
2999
3465
|
}
|
|
3000
3466
|
getWorkspaceRoot(issueIid) {
|
|
3001
|
-
return
|
|
3467
|
+
return path8.join(this.worktreeBaseDir, `issue-${issueIid}`);
|
|
3002
3468
|
}
|
|
3003
3469
|
// ── Internal helpers ──
|
|
3004
3470
|
async preparePrimaryRepo(issueIid, branchName, wsRoot, globalBaseBranch) {
|
|
3005
3471
|
const primary = this.wsConfig.primary;
|
|
3006
|
-
const repoDir =
|
|
3472
|
+
const repoDir = path8.join(wsRoot, primary.name);
|
|
3007
3473
|
const baseBranch = primary.baseBranch ?? globalBaseBranch;
|
|
3008
3474
|
await this.ensurePrimaryWorktree(repoDir, branchName, baseBranch);
|
|
3009
3475
|
return {
|
|
@@ -3011,18 +3477,18 @@ var WorkspaceManager = class {
|
|
|
3011
3477
|
projectPath: primary.projectPath,
|
|
3012
3478
|
role: primary.role ?? "",
|
|
3013
3479
|
gitRootDir: repoDir,
|
|
3014
|
-
workDir:
|
|
3480
|
+
workDir: path8.join(repoDir, primary.projectSubDir ?? ""),
|
|
3015
3481
|
baseBranch,
|
|
3016
3482
|
branchPrefix: primary.branchPrefix ?? "feat/issue",
|
|
3017
3483
|
isPrimary: true
|
|
3018
3484
|
};
|
|
3019
3485
|
}
|
|
3020
3486
|
async ensurePrimaryWorktree(repoDir, branchName, baseBranch) {
|
|
3021
|
-
const wsRoot =
|
|
3487
|
+
const wsRoot = path8.dirname(repoDir);
|
|
3022
3488
|
if (wsRoot !== repoDir) {
|
|
3023
3489
|
try {
|
|
3024
|
-
await
|
|
3025
|
-
|
|
3490
|
+
await fs7.access(path8.join(wsRoot, ".git"));
|
|
3491
|
+
logger11.info("Migrating legacy worktree to primary subdir", { from: wsRoot, to: repoDir });
|
|
3026
3492
|
await this.mainGit.worktreeRemove(wsRoot, true);
|
|
3027
3493
|
await this.mainGit.worktreePrune();
|
|
3028
3494
|
await this.cleanStaleDir(wsRoot);
|
|
@@ -3032,11 +3498,11 @@ var WorkspaceManager = class {
|
|
|
3032
3498
|
const worktrees = await this.mainGit.worktreeList();
|
|
3033
3499
|
if (worktrees.includes(repoDir)) {
|
|
3034
3500
|
try {
|
|
3035
|
-
await
|
|
3036
|
-
|
|
3501
|
+
await fs7.access(path8.join(repoDir, ".git"));
|
|
3502
|
+
logger11.info("Reusing existing primary worktree", { dir: repoDir });
|
|
3037
3503
|
return;
|
|
3038
3504
|
} catch {
|
|
3039
|
-
|
|
3505
|
+
logger11.warn("Primary worktree registered but .git missing, recreating", { dir: repoDir });
|
|
3040
3506
|
await this.mainGit.worktreeRemove(repoDir, true);
|
|
3041
3507
|
await this.mainGit.worktreePrune();
|
|
3042
3508
|
}
|
|
@@ -3055,19 +3521,19 @@ var WorkspaceManager = class {
|
|
|
3055
3521
|
await this.mainGit.worktreeAdd(repoDir, branchName, `origin/${baseBranch}`);
|
|
3056
3522
|
}
|
|
3057
3523
|
async prepareAssociateRepo(assoc, _issueIid, branchName, wsRoot, globalBaseBranch, globalBranchPrefix) {
|
|
3058
|
-
const repoDir =
|
|
3524
|
+
const repoDir = path8.join(wsRoot, assoc.name);
|
|
3059
3525
|
const baseBranch = assoc.baseBranch ?? globalBaseBranch;
|
|
3060
3526
|
const cloneUrl = `${this.gongfengApiUrl}/${assoc.projectPath}.git`;
|
|
3061
|
-
const gitDirExists = await this.dirExists(
|
|
3527
|
+
const gitDirExists = await this.dirExists(path8.join(repoDir, ".git"));
|
|
3062
3528
|
if (!gitDirExists) {
|
|
3063
3529
|
await this.cleanStaleDir(repoDir);
|
|
3064
|
-
|
|
3530
|
+
logger11.info("Cloning associate repo", { name: assoc.name, url: cloneUrl });
|
|
3065
3531
|
await execFileAsync("git", ["clone", "--depth", "50", cloneUrl, repoDir], {
|
|
3066
3532
|
timeout: 3e5,
|
|
3067
3533
|
maxBuffer: 10 * 1024 * 1024
|
|
3068
3534
|
});
|
|
3069
3535
|
} else {
|
|
3070
|
-
|
|
3536
|
+
logger11.info("Reusing existing associate clone", { name: assoc.name, dir: repoDir });
|
|
3071
3537
|
}
|
|
3072
3538
|
const assocGit = new GitOperations(repoDir);
|
|
3073
3539
|
await assocGit.fetch();
|
|
@@ -3090,7 +3556,7 @@ var WorkspaceManager = class {
|
|
|
3090
3556
|
projectPath: assoc.projectPath,
|
|
3091
3557
|
role: assoc.role ?? "",
|
|
3092
3558
|
gitRootDir: repoDir,
|
|
3093
|
-
workDir:
|
|
3559
|
+
workDir: path8.join(repoDir, assoc.projectSubDir ?? ""),
|
|
3094
3560
|
baseBranch,
|
|
3095
3561
|
branchPrefix: assoc.branchPrefix ?? globalBranchPrefix,
|
|
3096
3562
|
isPrimary: false
|
|
@@ -3098,13 +3564,13 @@ var WorkspaceManager = class {
|
|
|
3098
3564
|
}
|
|
3099
3565
|
async cleanStaleDir(dir) {
|
|
3100
3566
|
if (await this.dirExists(dir)) {
|
|
3101
|
-
|
|
3102
|
-
await
|
|
3567
|
+
logger11.warn("Removing stale directory", { dir });
|
|
3568
|
+
await fs7.rm(dir, { recursive: true, force: true });
|
|
3103
3569
|
}
|
|
3104
3570
|
}
|
|
3105
3571
|
async dirExists(dir) {
|
|
3106
3572
|
try {
|
|
3107
|
-
await
|
|
3573
|
+
await fs7.access(dir);
|
|
3108
3574
|
return true;
|
|
3109
3575
|
} catch {
|
|
3110
3576
|
return false;
|
|
@@ -3113,8 +3579,8 @@ var WorkspaceManager = class {
|
|
|
3113
3579
|
};
|
|
3114
3580
|
|
|
3115
3581
|
// src/orchestrator/PipelineOrchestrator.ts
|
|
3116
|
-
import
|
|
3117
|
-
import
|
|
3582
|
+
import path12 from "path";
|
|
3583
|
+
import fs11 from "fs/promises";
|
|
3118
3584
|
import fsSync from "fs";
|
|
3119
3585
|
import { execFile as execFile2 } from "child_process";
|
|
3120
3586
|
import { promisify as promisify2 } from "util";
|
|
@@ -3147,8 +3613,8 @@ function mapSupplement(s) {
|
|
|
3147
3613
|
}
|
|
3148
3614
|
|
|
3149
3615
|
// src/utils/MergeRequestHelper.ts
|
|
3150
|
-
import
|
|
3151
|
-
import
|
|
3616
|
+
import fs8 from "fs";
|
|
3617
|
+
import path9 from "path";
|
|
3152
3618
|
var TAPD_PATTERNS = [
|
|
3153
3619
|
/--story=(\d+)/i,
|
|
3154
3620
|
/--bug=(\d+)/i,
|
|
@@ -3192,9 +3658,9 @@ function generateMRDescription(options) {
|
|
|
3192
3658
|
];
|
|
3193
3659
|
const planSections = [];
|
|
3194
3660
|
for (const { filename, label } of summaryFiles) {
|
|
3195
|
-
const filePath =
|
|
3196
|
-
if (
|
|
3197
|
-
const content =
|
|
3661
|
+
const filePath = path9.join(planDir, ".claude-plan", `issue-${issueIid}`, filename);
|
|
3662
|
+
if (fs8.existsSync(filePath)) {
|
|
3663
|
+
const content = fs8.readFileSync(filePath, "utf-8");
|
|
3198
3664
|
const summary = extractSummary(content);
|
|
3199
3665
|
if (summary) {
|
|
3200
3666
|
planSections.push(`### ${label}
|
|
@@ -3218,7 +3684,7 @@ function extractSummary(content, maxLines = 20) {
|
|
|
3218
3684
|
|
|
3219
3685
|
// src/deploy/PortAllocator.ts
|
|
3220
3686
|
import net from "net";
|
|
3221
|
-
var
|
|
3687
|
+
var logger12 = logger.child("PortAllocator");
|
|
3222
3688
|
var DEFAULT_OPTIONS = {
|
|
3223
3689
|
backendPortBase: 4e3,
|
|
3224
3690
|
frontendPortBase: 9e3,
|
|
@@ -3243,7 +3709,7 @@ var PortAllocator = class {
|
|
|
3243
3709
|
async allocate(issueIid) {
|
|
3244
3710
|
const existing = this.allocated.get(issueIid);
|
|
3245
3711
|
if (existing) {
|
|
3246
|
-
|
|
3712
|
+
logger12.info("Returning already allocated ports", { issueIid, ports: existing });
|
|
3247
3713
|
return existing;
|
|
3248
3714
|
}
|
|
3249
3715
|
const usedBackend = new Set([...this.allocated.values()].map((p) => p.backendPort));
|
|
@@ -3261,10 +3727,10 @@ var PortAllocator = class {
|
|
|
3261
3727
|
if (beOk && feOk) {
|
|
3262
3728
|
const pair = { backendPort, frontendPort };
|
|
3263
3729
|
this.allocated.set(issueIid, pair);
|
|
3264
|
-
|
|
3730
|
+
logger12.info("Ports allocated", { issueIid, ...pair });
|
|
3265
3731
|
return pair;
|
|
3266
3732
|
}
|
|
3267
|
-
|
|
3733
|
+
logger12.debug("Port pair unavailable, trying next", {
|
|
3268
3734
|
backendPort,
|
|
3269
3735
|
frontendPort,
|
|
3270
3736
|
beOk,
|
|
@@ -3279,7 +3745,7 @@ var PortAllocator = class {
|
|
|
3279
3745
|
const pair = this.allocated.get(issueIid);
|
|
3280
3746
|
if (pair) {
|
|
3281
3747
|
this.allocated.delete(issueIid);
|
|
3282
|
-
|
|
3748
|
+
logger12.info("Ports released", { issueIid, ...pair });
|
|
3283
3749
|
}
|
|
3284
3750
|
}
|
|
3285
3751
|
getPortsForIssue(issueIid) {
|
|
@@ -3290,15 +3756,15 @@ var PortAllocator = class {
|
|
|
3290
3756
|
}
|
|
3291
3757
|
restore(issueIid, ports) {
|
|
3292
3758
|
this.allocated.set(issueIid, ports);
|
|
3293
|
-
|
|
3759
|
+
logger12.info("Ports restored from persistence", { issueIid, ...ports });
|
|
3294
3760
|
}
|
|
3295
3761
|
};
|
|
3296
3762
|
|
|
3297
3763
|
// src/deploy/DevServerManager.ts
|
|
3298
3764
|
import { spawn } from "child_process";
|
|
3299
|
-
import
|
|
3300
|
-
import
|
|
3301
|
-
var
|
|
3765
|
+
import fs9 from "fs";
|
|
3766
|
+
import path10 from "path";
|
|
3767
|
+
var logger13 = logger.child("DevServerManager");
|
|
3302
3768
|
var DEFAULT_OPTIONS2 = {};
|
|
3303
3769
|
var DevServerManager = class {
|
|
3304
3770
|
servers = /* @__PURE__ */ new Map();
|
|
@@ -3306,25 +3772,25 @@ var DevServerManager = class {
|
|
|
3306
3772
|
logDir;
|
|
3307
3773
|
constructor(options) {
|
|
3308
3774
|
this.options = { ...DEFAULT_OPTIONS2, ...options };
|
|
3309
|
-
this.logDir =
|
|
3310
|
-
if (!
|
|
3311
|
-
|
|
3775
|
+
this.logDir = path10.join(resolveDataDir(), "preview-logs");
|
|
3776
|
+
if (!fs9.existsSync(this.logDir)) {
|
|
3777
|
+
fs9.mkdirSync(this.logDir, { recursive: true });
|
|
3312
3778
|
}
|
|
3313
3779
|
}
|
|
3314
3780
|
getLogPath(issueIid, type) {
|
|
3315
|
-
const filePath =
|
|
3316
|
-
return
|
|
3781
|
+
const filePath = path10.join(this.logDir, `${issueIid}-${type}.log`);
|
|
3782
|
+
return fs9.existsSync(filePath) ? filePath : null;
|
|
3317
3783
|
}
|
|
3318
3784
|
async startServers(wtCtx, ports) {
|
|
3319
3785
|
if (this.servers.has(wtCtx.issueIid)) {
|
|
3320
|
-
|
|
3786
|
+
logger13.info("Servers already running for issue", { issueIid: wtCtx.issueIid });
|
|
3321
3787
|
return;
|
|
3322
3788
|
}
|
|
3323
|
-
|
|
3324
|
-
const backendLogPath =
|
|
3325
|
-
const frontendLogPath =
|
|
3326
|
-
const backendLog =
|
|
3327
|
-
const frontendLog =
|
|
3789
|
+
logger13.info("Starting dev servers", { issueIid: wtCtx.issueIid, ...ports });
|
|
3790
|
+
const backendLogPath = path10.join(this.logDir, `${wtCtx.issueIid}-backend.log`);
|
|
3791
|
+
const frontendLogPath = path10.join(this.logDir, `${wtCtx.issueIid}-frontend.log`);
|
|
3792
|
+
const backendLog = fs9.createWriteStream(backendLogPath, { flags: "a" });
|
|
3793
|
+
const frontendLog = fs9.createWriteStream(frontendLogPath, { flags: "a" });
|
|
3328
3794
|
const tsLine = (stream, data) => `[${(/* @__PURE__ */ new Date()).toISOString()}] [${stream}] ${data.toString().trimEnd()}
|
|
3329
3795
|
`;
|
|
3330
3796
|
const backendEnv = {
|
|
@@ -3348,9 +3814,9 @@ var DevServerManager = class {
|
|
|
3348
3814
|
backendLog.write(tsLine("stderr", data));
|
|
3349
3815
|
});
|
|
3350
3816
|
backend.on("exit", (code) => {
|
|
3351
|
-
|
|
3817
|
+
logger13.info("Backend process exited", { issueIid: wtCtx.issueIid, code });
|
|
3352
3818
|
});
|
|
3353
|
-
const frontendDir =
|
|
3819
|
+
const frontendDir = path10.join(wtCtx.workDir, "frontend");
|
|
3354
3820
|
const frontendEnv = {
|
|
3355
3821
|
...process.env,
|
|
3356
3822
|
BACKEND_PORT: String(ports.backendPort),
|
|
@@ -3372,7 +3838,7 @@ var DevServerManager = class {
|
|
|
3372
3838
|
frontendLog.write(tsLine("stderr", data));
|
|
3373
3839
|
});
|
|
3374
3840
|
frontend.on("exit", (code) => {
|
|
3375
|
-
|
|
3841
|
+
logger13.info("Frontend process exited", { issueIid: wtCtx.issueIid, code });
|
|
3376
3842
|
});
|
|
3377
3843
|
const serverSet = {
|
|
3378
3844
|
backend,
|
|
@@ -3384,14 +3850,14 @@ var DevServerManager = class {
|
|
|
3384
3850
|
frontendLog
|
|
3385
3851
|
};
|
|
3386
3852
|
this.servers.set(wtCtx.issueIid, serverSet);
|
|
3387
|
-
|
|
3853
|
+
logger13.info("Dev servers spawned, waiting for startup", { issueIid: wtCtx.issueIid, ...ports });
|
|
3388
3854
|
await new Promise((r) => setTimeout(r, 1e4));
|
|
3389
|
-
|
|
3855
|
+
logger13.info("Dev servers startup grace period done", { issueIid: wtCtx.issueIid });
|
|
3390
3856
|
}
|
|
3391
3857
|
stopServers(issueIid) {
|
|
3392
3858
|
const set = this.servers.get(issueIid);
|
|
3393
3859
|
if (!set) return;
|
|
3394
|
-
|
|
3860
|
+
logger13.info("Stopping dev servers", { issueIid, ports: set.ports });
|
|
3395
3861
|
killProcess(set.backend, `backend #${issueIid}`);
|
|
3396
3862
|
killProcess(set.frontend, `frontend #${issueIid}`);
|
|
3397
3863
|
set.backendLog.end();
|
|
@@ -3428,7 +3894,7 @@ function killProcess(proc, label) {
|
|
|
3428
3894
|
}
|
|
3429
3895
|
setTimeout(() => {
|
|
3430
3896
|
if (!proc.killed && proc.exitCode === null) {
|
|
3431
|
-
|
|
3897
|
+
logger13.warn(`Force killing ${label}`);
|
|
3432
3898
|
try {
|
|
3433
3899
|
process.kill(-pid, "SIGKILL");
|
|
3434
3900
|
} catch {
|
|
@@ -3437,7 +3903,7 @@ function killProcess(proc, label) {
|
|
|
3437
3903
|
}
|
|
3438
3904
|
}, 5e3);
|
|
3439
3905
|
} catch (err) {
|
|
3440
|
-
|
|
3906
|
+
logger13.warn(`Failed to kill ${label}`, { error: err.message });
|
|
3441
3907
|
}
|
|
3442
3908
|
}
|
|
3443
3909
|
|
|
@@ -3456,13 +3922,13 @@ function isE2eEnabledForIssue(issueIid, tracker, cfg) {
|
|
|
3456
3922
|
}
|
|
3457
3923
|
|
|
3458
3924
|
// src/e2e/ScreenshotCollector.ts
|
|
3459
|
-
import
|
|
3460
|
-
import
|
|
3461
|
-
var
|
|
3925
|
+
import fs10 from "fs";
|
|
3926
|
+
import path11 from "path";
|
|
3927
|
+
var logger14 = logger.child("ScreenshotCollector");
|
|
3462
3928
|
var MAX_SCREENSHOTS = 20;
|
|
3463
3929
|
function walkDir(dir, files = []) {
|
|
3464
|
-
for (const entry of
|
|
3465
|
-
const full =
|
|
3930
|
+
for (const entry of fs10.readdirSync(dir, { withFileTypes: true })) {
|
|
3931
|
+
const full = path11.join(dir, entry.name);
|
|
3466
3932
|
if (entry.isDirectory()) {
|
|
3467
3933
|
walkDir(full, files);
|
|
3468
3934
|
} else if (entry.isFile() && entry.name.endsWith(".png")) {
|
|
@@ -3472,34 +3938,34 @@ function walkDir(dir, files = []) {
|
|
|
3472
3938
|
return files;
|
|
3473
3939
|
}
|
|
3474
3940
|
function collectScreenshots(workDir) {
|
|
3475
|
-
const testResultsDir =
|
|
3476
|
-
if (!
|
|
3477
|
-
|
|
3941
|
+
const testResultsDir = path11.join(workDir, "frontend", "test-results");
|
|
3942
|
+
if (!fs10.existsSync(testResultsDir)) {
|
|
3943
|
+
logger14.debug("test-results directory not found", { dir: testResultsDir });
|
|
3478
3944
|
return [];
|
|
3479
3945
|
}
|
|
3480
3946
|
const pngFiles = walkDir(testResultsDir);
|
|
3481
3947
|
if (pngFiles.length === 0) {
|
|
3482
|
-
|
|
3948
|
+
logger14.debug("No screenshots found");
|
|
3483
3949
|
return [];
|
|
3484
3950
|
}
|
|
3485
3951
|
const screenshots = pngFiles.map((filePath) => {
|
|
3486
|
-
const relative =
|
|
3487
|
-
const testName = relative.split(
|
|
3952
|
+
const relative = path11.relative(testResultsDir, filePath);
|
|
3953
|
+
const testName = relative.split(path11.sep)[0] || path11.basename(filePath, ".png");
|
|
3488
3954
|
return { filePath, testName };
|
|
3489
3955
|
});
|
|
3490
3956
|
if (screenshots.length > MAX_SCREENSHOTS) {
|
|
3491
|
-
|
|
3957
|
+
logger14.warn("Too many screenshots, truncating", {
|
|
3492
3958
|
total: screenshots.length,
|
|
3493
3959
|
max: MAX_SCREENSHOTS
|
|
3494
3960
|
});
|
|
3495
3961
|
return screenshots.slice(0, MAX_SCREENSHOTS);
|
|
3496
3962
|
}
|
|
3497
|
-
|
|
3963
|
+
logger14.info("Screenshots collected", { count: screenshots.length });
|
|
3498
3964
|
return screenshots;
|
|
3499
3965
|
}
|
|
3500
3966
|
|
|
3501
3967
|
// src/e2e/ScreenshotPublisher.ts
|
|
3502
|
-
var
|
|
3968
|
+
var logger15 = logger.child("ScreenshotPublisher");
|
|
3503
3969
|
function buildComment(uploaded, truncated) {
|
|
3504
3970
|
const lines = [t("screenshot.title"), ""];
|
|
3505
3971
|
for (const item of uploaded) {
|
|
@@ -3518,12 +3984,12 @@ var ScreenshotPublisher = class {
|
|
|
3518
3984
|
const { workDir, issueIid, issueId, mrIid } = options;
|
|
3519
3985
|
const screenshots = collectScreenshots(workDir);
|
|
3520
3986
|
if (screenshots.length === 0) {
|
|
3521
|
-
|
|
3987
|
+
logger15.info("No E2E screenshots to publish", { issueIid });
|
|
3522
3988
|
return;
|
|
3523
3989
|
}
|
|
3524
3990
|
const uploaded = await this.uploadAll(screenshots);
|
|
3525
3991
|
if (uploaded.length === 0) {
|
|
3526
|
-
|
|
3992
|
+
logger15.warn("All screenshot uploads failed", { issueIid });
|
|
3527
3993
|
return;
|
|
3528
3994
|
}
|
|
3529
3995
|
const truncated = screenshots.length >= 20;
|
|
@@ -3532,7 +3998,7 @@ var ScreenshotPublisher = class {
|
|
|
3532
3998
|
if (mrIid) {
|
|
3533
3999
|
await this.postToMergeRequest(mrIid, comment);
|
|
3534
4000
|
}
|
|
3535
|
-
|
|
4001
|
+
logger15.info("E2E screenshots published", {
|
|
3536
4002
|
issueIid,
|
|
3537
4003
|
mrIid,
|
|
3538
4004
|
count: uploaded.length
|
|
@@ -3548,7 +4014,7 @@ var ScreenshotPublisher = class {
|
|
|
3548
4014
|
markdown: result.markdown
|
|
3549
4015
|
});
|
|
3550
4016
|
} catch (err) {
|
|
3551
|
-
|
|
4017
|
+
logger15.warn("Failed to upload screenshot", {
|
|
3552
4018
|
filePath: screenshot.filePath,
|
|
3553
4019
|
error: err.message
|
|
3554
4020
|
});
|
|
@@ -3560,7 +4026,7 @@ var ScreenshotPublisher = class {
|
|
|
3560
4026
|
try {
|
|
3561
4027
|
await this.gongfeng.createIssueNote(issueId, comment);
|
|
3562
4028
|
} catch (err) {
|
|
3563
|
-
|
|
4029
|
+
logger15.warn("Failed to post screenshots to issue", {
|
|
3564
4030
|
issueId,
|
|
3565
4031
|
error: err.message
|
|
3566
4032
|
});
|
|
@@ -3570,7 +4036,7 @@ var ScreenshotPublisher = class {
|
|
|
3570
4036
|
try {
|
|
3571
4037
|
await this.gongfeng.createMergeRequestNote(mrIid, comment);
|
|
3572
4038
|
} catch (err) {
|
|
3573
|
-
|
|
4039
|
+
logger15.warn("Failed to post screenshots to merge request", {
|
|
3574
4040
|
mrIid,
|
|
3575
4041
|
error: err.message
|
|
3576
4042
|
});
|
|
@@ -3753,7 +4219,7 @@ metrics.registerCounter("iaf_braindump_batches_total", "Total braindump batches"
|
|
|
3753
4219
|
metrics.registerCounter("iaf_braindump_tasks_total", "Total braindump tasks");
|
|
3754
4220
|
|
|
3755
4221
|
// src/orchestrator/steps/SetupStep.ts
|
|
3756
|
-
var
|
|
4222
|
+
var logger16 = logger.child("SetupStep");
|
|
3757
4223
|
async function executeSetup(ctx, deps) {
|
|
3758
4224
|
const { issue, wtCtx, record, pipelineDef, branchName } = ctx;
|
|
3759
4225
|
try {
|
|
@@ -3762,7 +4228,7 @@ async function executeSetup(ctx, deps) {
|
|
|
3762
4228
|
"auto-finish:processing"
|
|
3763
4229
|
]);
|
|
3764
4230
|
} catch (err) {
|
|
3765
|
-
|
|
4231
|
+
logger16.warn("Failed to update issue labels", { error: err.message });
|
|
3766
4232
|
}
|
|
3767
4233
|
await deps.mainGitMutex.runExclusive(async () => {
|
|
3768
4234
|
deps.emitProgress(issue.iid, "fetch", t("orchestrator.fetchProgress"));
|
|
@@ -3804,6 +4270,28 @@ async function executeSetup(ctx, deps) {
|
|
|
3804
4270
|
if (!record.phaseProgress) {
|
|
3805
4271
|
deps.tracker.initPhaseProgress(issue.iid, pipelineDef);
|
|
3806
4272
|
}
|
|
4273
|
+
const allArtifacts = pipelineDef.phases.flatMap((p) => p.artifacts ?? []).map((a) => a.filename);
|
|
4274
|
+
if (allArtifacts.length > 0) {
|
|
4275
|
+
try {
|
|
4276
|
+
const firstPhase = pipelineDef.phases.find((p) => p.kind === "ai");
|
|
4277
|
+
const firstPhaseArtifacts = (firstPhase?.artifacts ?? []).map((a) => a.filename);
|
|
4278
|
+
const hookInjector = new HookInjector();
|
|
4279
|
+
hookInjector.inject({
|
|
4280
|
+
workDir: primaryWorkDir,
|
|
4281
|
+
planDir: wtPlan.planDir,
|
|
4282
|
+
expectedArtifacts: allArtifacts,
|
|
4283
|
+
phaseExpectedArtifacts: firstPhaseArtifacts,
|
|
4284
|
+
issueIid: issue.iid,
|
|
4285
|
+
phaseName: firstPhase?.name,
|
|
4286
|
+
issueTitle: issue.title,
|
|
4287
|
+
issueDescription: issue.description
|
|
4288
|
+
});
|
|
4289
|
+
} catch (err) {
|
|
4290
|
+
logger16.warn("Failed to inject Claude Code hooks (non-blocking)", {
|
|
4291
|
+
error: err.message
|
|
4292
|
+
});
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
3807
4295
|
const wtGitMap = /* @__PURE__ */ new Map();
|
|
3808
4296
|
if (wtCtx.workspace) {
|
|
3809
4297
|
wtGitMap.set(wtCtx.workspace.primary.name, wtGit);
|
|
@@ -3816,6 +4304,44 @@ async function executeSetup(ctx, deps) {
|
|
|
3816
4304
|
return { wtGit, wtPlan, wtGitMap };
|
|
3817
4305
|
}
|
|
3818
4306
|
|
|
4307
|
+
// src/lifecycle/FeedbackTypes.ts
|
|
4308
|
+
function inputRequestToFeedback(request) {
|
|
4309
|
+
switch (request.type) {
|
|
4310
|
+
case "interactive-dialog":
|
|
4311
|
+
return {
|
|
4312
|
+
kind: "interactive-dialog",
|
|
4313
|
+
question: request.content,
|
|
4314
|
+
options: request.options ?? []
|
|
4315
|
+
};
|
|
4316
|
+
case "plan-approval":
|
|
4317
|
+
return { kind: "approval-required", scope: "plan" };
|
|
4318
|
+
case "generic":
|
|
4319
|
+
return { kind: "generic", content: request.content };
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
function feedbackResponseToString(response) {
|
|
4323
|
+
switch (response.action) {
|
|
4324
|
+
case "approve":
|
|
4325
|
+
return "allow";
|
|
4326
|
+
case "reject":
|
|
4327
|
+
return "reject";
|
|
4328
|
+
case "select":
|
|
4329
|
+
return response.value;
|
|
4330
|
+
case "dismiss":
|
|
4331
|
+
return "";
|
|
4332
|
+
case "pause":
|
|
4333
|
+
return "";
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
function stringToFeedbackResponse(str, originalFeedback) {
|
|
4337
|
+
if (str === "allow") return { action: "approve" };
|
|
4338
|
+
if (str === "reject") return { action: "reject" };
|
|
4339
|
+
if (str === "") {
|
|
4340
|
+
return originalFeedback.kind === "interactive-dialog" ? { action: "dismiss" } : { action: "approve" };
|
|
4341
|
+
}
|
|
4342
|
+
return { action: "select", value: str };
|
|
4343
|
+
}
|
|
4344
|
+
|
|
3819
4345
|
// src/notesync/NoteSyncSettings.ts
|
|
3820
4346
|
var noteSyncOverride;
|
|
3821
4347
|
function getNoteSyncEnabled(cfg) {
|
|
@@ -3872,8 +4398,14 @@ function clearPendingDialog(issueIid) {
|
|
|
3872
4398
|
store.delete(issueIid);
|
|
3873
4399
|
}
|
|
3874
4400
|
|
|
3875
|
-
// src/orchestrator/steps/
|
|
3876
|
-
var
|
|
4401
|
+
// src/orchestrator/steps/PhaseHelpers.ts
|
|
4402
|
+
var logger17 = logger.child("PhaseHelpers");
|
|
4403
|
+
async function safeComment(deps, issueId, message) {
|
|
4404
|
+
try {
|
|
4405
|
+
await deps.gongfeng.createIssueNote(issueId, message);
|
|
4406
|
+
} catch {
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
3877
4409
|
function resolveVerifyRunner(deps) {
|
|
3878
4410
|
return deps.aiRunner;
|
|
3879
4411
|
}
|
|
@@ -3889,7 +4421,7 @@ async function commitPlanFiles(ctx, wtGit, wtGitMap, phaseName, displayId) {
|
|
|
3889
4421
|
for (const repo of ctx.workspace.repos) {
|
|
3890
4422
|
const repoGit = wtGitMap?.get(repo.name);
|
|
3891
4423
|
if (!repoGit) {
|
|
3892
|
-
|
|
4424
|
+
logger17.warn("Missing GitOperations for repo, skipping commit", { repo: repo.name });
|
|
3893
4425
|
continue;
|
|
3894
4426
|
}
|
|
3895
4427
|
const branch = repo.branchPrefix ? `${repo.branchPrefix}-${displayId}` : ctx.branchName;
|
|
@@ -3898,10 +4430,10 @@ async function commitPlanFiles(ctx, wtGit, wtGitMap, phaseName, displayId) {
|
|
|
3898
4430
|
await repoGit.add(["."]);
|
|
3899
4431
|
await repoGit.commit(commitMsg);
|
|
3900
4432
|
await repoGit.push(branch);
|
|
3901
|
-
|
|
4433
|
+
logger17.info("Committed changes for repo", { repo: repo.name, branch });
|
|
3902
4434
|
}
|
|
3903
4435
|
} catch (err) {
|
|
3904
|
-
|
|
4436
|
+
logger17.warn("Failed to commit/push for repo", {
|
|
3905
4437
|
repo: repo.name,
|
|
3906
4438
|
error: err.message
|
|
3907
4439
|
});
|
|
@@ -3939,27 +4471,15 @@ async function syncResultToIssue(phase, ctx, displayId, phaseName, deps, issueId
|
|
|
3939
4471
|
summary
|
|
3940
4472
|
);
|
|
3941
4473
|
await safeComment(deps, issueId, comment);
|
|
3942
|
-
|
|
4474
|
+
logger17.info("Result synced to issue", { issueIid: displayId, file: file.filename });
|
|
3943
4475
|
}
|
|
3944
4476
|
} catch (err) {
|
|
3945
|
-
|
|
4477
|
+
logger17.warn("Failed to sync result to issue", { error: err.message });
|
|
3946
4478
|
await safeComment(deps, issueId, issueProgressComment(phaseName, "completed"));
|
|
3947
4479
|
}
|
|
3948
4480
|
}
|
|
3949
|
-
function buildInputHandler(displayId, phaseName, deps) {
|
|
3950
|
-
const useAcpGate = deps.config.ai.codebuddyAcpAutoApprove === false;
|
|
3951
|
-
return (request) => {
|
|
3952
|
-
if (request.type === "interactive-dialog") {
|
|
3953
|
-
return handleInteractiveDialog(displayId, phaseName, deps, request);
|
|
3954
|
-
}
|
|
3955
|
-
if (request.type === "plan-approval" && useAcpGate) {
|
|
3956
|
-
return handlePlanApproval(displayId, phaseName, deps);
|
|
3957
|
-
}
|
|
3958
|
-
return Promise.resolve("allow");
|
|
3959
|
-
};
|
|
3960
|
-
}
|
|
3961
4481
|
function handlePlanApproval(displayId, phaseName, deps) {
|
|
3962
|
-
|
|
4482
|
+
logger17.info("ACP plan-approval requested, delegating to review gate", {
|
|
3963
4483
|
issueIid: displayId,
|
|
3964
4484
|
phase: phaseName
|
|
3965
4485
|
});
|
|
@@ -3969,14 +4489,14 @@ function handlePlanApproval(displayId, phaseName, deps) {
|
|
|
3969
4489
|
const data = payload.data;
|
|
3970
4490
|
if (data.issueIid !== displayId) return;
|
|
3971
4491
|
cleanup();
|
|
3972
|
-
|
|
4492
|
+
logger17.info("ACP plan-approval approved via review gate", { issueIid: displayId });
|
|
3973
4493
|
resolve("allow");
|
|
3974
4494
|
};
|
|
3975
4495
|
const onRejected = (payload) => {
|
|
3976
4496
|
const data = payload.data;
|
|
3977
4497
|
if (data.issueIid !== displayId) return;
|
|
3978
4498
|
cleanup();
|
|
3979
|
-
|
|
4499
|
+
logger17.info("ACP plan-approval rejected via review gate", { issueIid: displayId });
|
|
3980
4500
|
resolve("reject");
|
|
3981
4501
|
};
|
|
3982
4502
|
const cleanup = () => {
|
|
@@ -3988,7 +4508,7 @@ function handlePlanApproval(displayId, phaseName, deps) {
|
|
|
3988
4508
|
});
|
|
3989
4509
|
}
|
|
3990
4510
|
function handleInteractiveDialog(displayId, phaseName, deps, request) {
|
|
3991
|
-
|
|
4511
|
+
logger17.info("Interactive dialog forwarded to frontend", {
|
|
3992
4512
|
issueIid: displayId,
|
|
3993
4513
|
phase: phaseName,
|
|
3994
4514
|
question: request.content.slice(0, 80),
|
|
@@ -4018,7 +4538,7 @@ function handleInteractiveDialog(displayId, phaseName, deps, request) {
|
|
|
4018
4538
|
if (data.issueIid !== displayId) return;
|
|
4019
4539
|
cleanup();
|
|
4020
4540
|
clearPendingDialog(displayId);
|
|
4021
|
-
|
|
4541
|
+
logger17.info("Interactive dialog response received from frontend", {
|
|
4022
4542
|
issueIid: displayId,
|
|
4023
4543
|
response: data.response
|
|
4024
4544
|
});
|
|
@@ -4028,46 +4548,99 @@ function handleInteractiveDialog(displayId, phaseName, deps, request) {
|
|
|
4028
4548
|
const data = payload.data;
|
|
4029
4549
|
if (data.issueIid !== displayId) return;
|
|
4030
4550
|
cleanup();
|
|
4031
|
-
|
|
4551
|
+
logger17.info("Interactive dialog dismissed by user (false positive)", { issueIid: displayId });
|
|
4032
4552
|
resolve("");
|
|
4033
4553
|
};
|
|
4034
4554
|
deps.eventBus.on("agent:inputResponse", onResponse);
|
|
4035
4555
|
deps.eventBus.on("agent:dialogDismissed", onDismiss);
|
|
4036
4556
|
});
|
|
4037
4557
|
}
|
|
4038
|
-
|
|
4558
|
+
function updateHooksForPhase(spec, pipelineDef, ctx, wtPlan) {
|
|
4559
|
+
const phaseArtifacts = (spec.artifacts ?? []).map((a) => a.filename);
|
|
4560
|
+
if (phaseArtifacts.length === 0 && spec.kind !== "ai") return;
|
|
4039
4561
|
try {
|
|
4040
|
-
|
|
4041
|
-
|
|
4562
|
+
const allArtifacts = pipelineDef.phases.flatMap((p) => p.artifacts ?? []).map((a) => a.filename);
|
|
4563
|
+
const hookInjector = new HookInjector();
|
|
4564
|
+
hookInjector.updateForPhase({
|
|
4565
|
+
workDir: wtPlan.baseDir,
|
|
4566
|
+
planDir: wtPlan.planDir,
|
|
4567
|
+
expectedArtifacts: allArtifacts.length > 0 ? allArtifacts : phaseArtifacts,
|
|
4568
|
+
phaseExpectedArtifacts: phaseArtifacts,
|
|
4569
|
+
issueIid: ctx.issue.iid,
|
|
4570
|
+
phaseName: spec.name,
|
|
4571
|
+
issueTitle: ctx.issue.title,
|
|
4572
|
+
issueDescription: ctx.issue.description
|
|
4573
|
+
});
|
|
4574
|
+
} catch (err) {
|
|
4575
|
+
logger17.warn("Failed to update hooks for phase (non-blocking)", {
|
|
4576
|
+
phase: spec.name,
|
|
4577
|
+
error: err.message
|
|
4578
|
+
});
|
|
4042
4579
|
}
|
|
4043
4580
|
}
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
onStreamEvent: (event) => deps.eventBus.emitTyped("agent:output", {
|
|
4581
|
+
|
|
4582
|
+
// src/lifecycle/DefaultLifecycleHook.ts
|
|
4583
|
+
var logger18 = logger.child("DefaultLifecycleHook");
|
|
4584
|
+
var DefaultLifecycleHook = class {
|
|
4585
|
+
async beforePhase(ctx) {
|
|
4586
|
+
const { spec, issueCtx, deps, wtPlan } = ctx;
|
|
4587
|
+
const { issue } = issueCtx;
|
|
4588
|
+
const displayId = issue.iid;
|
|
4589
|
+
deps.tracker.updateState(displayId, spec.startState, { currentPhase: spec.name });
|
|
4590
|
+
deps.tracker.updatePhaseProgress(displayId, spec.name, {
|
|
4591
|
+
status: "in_progress",
|
|
4592
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4593
|
+
});
|
|
4594
|
+
wtPlan.updatePhaseProgress(spec.name, "in_progress");
|
|
4595
|
+
await safeComment(deps, issue.id, issueProgressComment(spec.name, "in_progress"));
|
|
4596
|
+
const phaseLabel = t(`phase.${spec.name}`) || spec.name;
|
|
4597
|
+
deps.eventBus.emitTyped("agent:output", {
|
|
4062
4598
|
issueIid: displayId,
|
|
4063
4599
|
phase: spec.name,
|
|
4600
|
+
event: {
|
|
4601
|
+
type: "system",
|
|
4602
|
+
content: t("basePhase.aiStarting", { label: phaseLabel }),
|
|
4603
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4604
|
+
}
|
|
4605
|
+
});
|
|
4606
|
+
}
|
|
4607
|
+
onStream(ctx, event) {
|
|
4608
|
+
const { spec, issueCtx, deps } = ctx;
|
|
4609
|
+
deps.eventBus.emitTyped("agent:output", {
|
|
4610
|
+
issueIid: issueCtx.issue.iid,
|
|
4611
|
+
phase: spec.name,
|
|
4064
4612
|
event
|
|
4065
|
-
})
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4613
|
+
});
|
|
4614
|
+
}
|
|
4615
|
+
beforeExecute;
|
|
4616
|
+
afterExecute;
|
|
4617
|
+
async onFeedback(ctx, feedback) {
|
|
4618
|
+
const { deps, displayId, spec } = ctx;
|
|
4619
|
+
if (feedback.kind === "interactive-dialog") {
|
|
4620
|
+
const result = await handleInteractiveDialog(displayId, spec.name, deps, {
|
|
4621
|
+
type: "interactive-dialog",
|
|
4622
|
+
content: feedback.question,
|
|
4623
|
+
options: feedback.options
|
|
4624
|
+
});
|
|
4625
|
+
return stringToFeedbackResponse(result, feedback);
|
|
4626
|
+
}
|
|
4627
|
+
if (feedback.kind === "approval-required" && feedback.scope === "plan") {
|
|
4628
|
+
const useAcpGate = deps.config.ai.codebuddyAcpAutoApprove === false;
|
|
4629
|
+
if (useAcpGate) {
|
|
4630
|
+
const result = await handlePlanApproval(displayId, spec.name, deps);
|
|
4631
|
+
return result === "allow" ? { action: "approve" } : { action: "reject" };
|
|
4632
|
+
}
|
|
4633
|
+
return { action: "approve" };
|
|
4634
|
+
}
|
|
4635
|
+
return { action: "approve" };
|
|
4636
|
+
}
|
|
4637
|
+
async afterPhase(ctx, outcome) {
|
|
4638
|
+
const { spec, issueCtx, deps, wtGit, wtPlan, wtGitMap, phase } = ctx;
|
|
4639
|
+
const { issue, phaseCtx } = issueCtx;
|
|
4640
|
+
const displayId = issue.iid;
|
|
4641
|
+
if (outcome.sessionId) {
|
|
4642
|
+
wtPlan.updatePhaseSessionId(spec.name, outcome.sessionId);
|
|
4643
|
+
}
|
|
4071
4644
|
deps.tracker.updateState(displayId, spec.doneState, { currentPhase: spec.name });
|
|
4072
4645
|
deps.tracker.updatePhaseProgress(displayId, spec.name, {
|
|
4073
4646
|
status: "completed",
|
|
@@ -4075,28 +4648,400 @@ async function runPhaseWithLifecycle(phase, phaseCtx, spec, ctx, deps, wtGit, wt
|
|
|
4075
4648
|
});
|
|
4076
4649
|
wtPlan.updatePhaseProgress(spec.name, "completed");
|
|
4077
4650
|
await commitPlanFiles(phaseCtx, wtGit, wtGitMap, spec.name, displayId);
|
|
4078
|
-
|
|
4079
|
-
|
|
4651
|
+
if (phase) {
|
|
4652
|
+
await syncResultToIssue(phase, phaseCtx, displayId, spec.name, deps, issue.id, wtPlan);
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
4655
|
+
async onError(ctx, error) {
|
|
4656
|
+
const { spec, issueCtx, deps, wtPlan } = ctx;
|
|
4657
|
+
const { issue } = issueCtx;
|
|
4658
|
+
const displayId = issue.iid;
|
|
4659
|
+
wtPlan.updatePhaseProgress(spec.name, "failed", error.message);
|
|
4660
|
+
deps.tracker.updatePhaseProgress(displayId, spec.name, { status: "failed" });
|
|
4661
|
+
if (error.wasActiveAtTimeout) {
|
|
4662
|
+
deps.tracker.markFailedSoft(displayId, error.message, "phase_running" /* PhaseRunning */);
|
|
4663
|
+
} else {
|
|
4664
|
+
deps.tracker.markFailed(displayId, error.message, "phase_running" /* PhaseRunning */);
|
|
4665
|
+
}
|
|
4666
|
+
const shortErr = error.message.slice(0, 200);
|
|
4667
|
+
await safeComment(deps, issue.id, issueProgressComment(spec.name, "failed", shortErr));
|
|
4668
|
+
return { action: "fail", softFail: error.wasActiveAtTimeout };
|
|
4080
4669
|
}
|
|
4081
|
-
|
|
4082
|
-
|
|
4670
|
+
};
|
|
4671
|
+
function createCallbacksFromHook(hook, ctx) {
|
|
4672
|
+
return {
|
|
4673
|
+
onStreamEvent: hook.onStream ? (event) => hook.onStream(ctx, event) : void 0,
|
|
4674
|
+
onInputRequired: hook.onFeedback ? async (request) => {
|
|
4675
|
+
const feedback = inputRequestToFeedback(request);
|
|
4676
|
+
const response = await hook.onFeedback(ctx, feedback);
|
|
4677
|
+
return feedbackResponseToString(response);
|
|
4678
|
+
} : void 0
|
|
4679
|
+
};
|
|
4680
|
+
}
|
|
4681
|
+
|
|
4682
|
+
// src/orchestrator/strategies/GateStrategy.ts
|
|
4683
|
+
var logger19 = logger.child("GateStrategy");
|
|
4684
|
+
var GateStrategy = class {
|
|
4685
|
+
name = "gate";
|
|
4686
|
+
shouldSkip() {
|
|
4687
|
+
return false;
|
|
4083
4688
|
}
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4689
|
+
async execute(ctx, _hooks) {
|
|
4690
|
+
const { spec, issueCtx, deps, wtPlan } = ctx;
|
|
4691
|
+
const { issue } = issueCtx;
|
|
4692
|
+
if (deps.shouldAutoApprove(issue.labels)) {
|
|
4693
|
+
logger19.info("Auto-approving review gate (matched autoApproveLabels)", {
|
|
4694
|
+
iid: issue.iid,
|
|
4695
|
+
labels: issue.labels,
|
|
4696
|
+
autoApproveLabels: deps.config.review.autoApproveLabels
|
|
4697
|
+
});
|
|
4698
|
+
if (spec.approvedState) {
|
|
4699
|
+
deps.tracker.updateState(issue.iid, spec.approvedState, { currentPhase: spec.name });
|
|
4700
|
+
}
|
|
4701
|
+
wtPlan.updatePhaseProgress(spec.name, "completed");
|
|
4702
|
+
deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
|
|
4703
|
+
status: "completed",
|
|
4704
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4705
|
+
});
|
|
4706
|
+
try {
|
|
4707
|
+
await deps.gongfeng.createIssueNote(issue.id, t("orchestrator.autoApproveComment"));
|
|
4708
|
+
} catch {
|
|
4709
|
+
}
|
|
4710
|
+
return { paused: false };
|
|
4711
|
+
}
|
|
4712
|
+
deps.tracker.updateState(issue.iid, spec.startState, { currentPhase: spec.name });
|
|
4713
|
+
wtPlan.updatePhaseProgress(spec.name, "in_progress");
|
|
4714
|
+
deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
|
|
4715
|
+
status: "in_progress",
|
|
4716
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4717
|
+
});
|
|
4718
|
+
deps.eventBus.emitTyped("review:requested", { issueIid: issue.iid });
|
|
4719
|
+
logger19.info("Review gate reached, pausing", { iid: issue.iid });
|
|
4720
|
+
return { paused: true };
|
|
4721
|
+
}
|
|
4722
|
+
};
|
|
4723
|
+
|
|
4724
|
+
// src/orchestrator/strategies/AiPhaseStrategy.ts
|
|
4725
|
+
var logger20 = logger.child("AiPhaseStrategy");
|
|
4726
|
+
var AiPhaseStrategy = class {
|
|
4727
|
+
name = "ai";
|
|
4728
|
+
shouldSkip(ctx) {
|
|
4729
|
+
const { spec, issueCtx, deps } = ctx;
|
|
4730
|
+
if (spec.name === "uat" && !isE2eEnabledForIssue(issueCtx.issue.iid, deps.tracker, deps.config)) {
|
|
4731
|
+
logger20.info("UAT phase skipped (E2E not enabled for this issue)", { iid: issueCtx.issue.iid });
|
|
4732
|
+
return true;
|
|
4733
|
+
}
|
|
4734
|
+
return false;
|
|
4093
4735
|
}
|
|
4094
|
-
|
|
4736
|
+
async execute(ctx, hooks) {
|
|
4737
|
+
const { spec, issueCtx, deps, wtGit, wtPlan, wtGitMap } = ctx;
|
|
4738
|
+
const { issue, phaseCtx } = issueCtx;
|
|
4739
|
+
if (this.shouldSkip(ctx)) {
|
|
4740
|
+
deps.tracker.updateState(issue.iid, spec.doneState, { currentPhase: spec.name });
|
|
4741
|
+
wtPlan.updatePhaseProgress(spec.name, "completed");
|
|
4742
|
+
deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
|
|
4743
|
+
status: "completed",
|
|
4744
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4745
|
+
});
|
|
4746
|
+
return { paused: false };
|
|
4747
|
+
}
|
|
4748
|
+
updateHooksForPhase(spec, issueCtx.pipelineDef, issueCtx, wtPlan);
|
|
4749
|
+
const runner = this.resolveRunner(ctx);
|
|
4750
|
+
if (spec.name === "uat") {
|
|
4751
|
+
const runnerName = runner === deps.e2eAiRunner ? "e2eAiRunner (CodeBuddy)" : "mainRunner";
|
|
4752
|
+
logger20.info("UAT phase starting", { iid: issue.iid, runner: runnerName });
|
|
4753
|
+
}
|
|
4754
|
+
const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config);
|
|
4755
|
+
if (wtGitMap) phase.setWtGitMap(wtGitMap);
|
|
4756
|
+
ctx.phase = phase;
|
|
4757
|
+
const outcome = await runPhaseWithLifecycle(
|
|
4758
|
+
phase,
|
|
4759
|
+
phaseCtx,
|
|
4760
|
+
spec,
|
|
4761
|
+
issueCtx,
|
|
4762
|
+
deps,
|
|
4763
|
+
wtGit,
|
|
4764
|
+
wtPlan,
|
|
4765
|
+
wtGitMap,
|
|
4766
|
+
hooks
|
|
4767
|
+
);
|
|
4768
|
+
if (outcome.status === "running") {
|
|
4769
|
+
return this.handleAsyncOutcome(ctx, outcome);
|
|
4770
|
+
}
|
|
4771
|
+
if (spec.approvedState && outcome.data?.hasReleaseCapability) {
|
|
4772
|
+
return this.handleGateRequest(ctx);
|
|
4773
|
+
}
|
|
4774
|
+
return { paused: false };
|
|
4775
|
+
}
|
|
4776
|
+
resolveRunner(ctx) {
|
|
4777
|
+
const { spec, deps, displayId } = ctx;
|
|
4778
|
+
if (spec.name === "verify") return resolveVerifyRunner(deps);
|
|
4779
|
+
if (spec.name === "uat") return resolveUatRunner(deps, displayId);
|
|
4780
|
+
return deps.aiRunner;
|
|
4781
|
+
}
|
|
4782
|
+
async handleAsyncOutcome(ctx, outcome) {
|
|
4783
|
+
const { spec, issueCtx, deps, wtGit, wtPlan, wtGitMap } = ctx;
|
|
4784
|
+
const { issue, phaseCtx } = issueCtx;
|
|
4785
|
+
if (outcome.awaitCompletion) {
|
|
4786
|
+
logger20.info("Async phase running, awaiting completion", { iid: issue.iid, phase: spec.name });
|
|
4787
|
+
const finalOutcome = await outcome.awaitCompletion;
|
|
4788
|
+
if (finalOutcome.sessionId) {
|
|
4789
|
+
wtPlan.updatePhaseSessionId(spec.name, finalOutcome.sessionId);
|
|
4790
|
+
}
|
|
4791
|
+
if (finalOutcome.status === "completed") {
|
|
4792
|
+
deps.tracker.updateState(issue.iid, spec.doneState, { currentPhase: spec.name });
|
|
4793
|
+
deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
|
|
4794
|
+
status: "completed",
|
|
4795
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4796
|
+
});
|
|
4797
|
+
wtPlan.updatePhaseProgress(spec.name, "completed");
|
|
4798
|
+
await commitPlanFiles(phaseCtx, wtGit, wtGitMap, spec.name, issue.iid);
|
|
4799
|
+
const runner = this.resolveRunner(ctx);
|
|
4800
|
+
const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config);
|
|
4801
|
+
await syncResultToIssue(phase, phaseCtx, issue.iid, spec.name, deps, issue.id, wtPlan);
|
|
4802
|
+
logger20.info("Async phase completed successfully", { iid: issue.iid, phase: spec.name });
|
|
4803
|
+
return { paused: false };
|
|
4804
|
+
}
|
|
4805
|
+
const errMsg = finalOutcome.error?.message ?? "Unknown error";
|
|
4806
|
+
const shortErr = errMsg.slice(0, 200);
|
|
4807
|
+
const wasActive = finalOutcome.error?.wasActiveAtTimeout ?? false;
|
|
4808
|
+
wtPlan.updatePhaseProgress(spec.name, "failed", errMsg);
|
|
4809
|
+
deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "failed" });
|
|
4810
|
+
if (wasActive) {
|
|
4811
|
+
deps.tracker.markFailedSoft(issue.iid, errMsg, "phase_running" /* PhaseRunning */);
|
|
4812
|
+
} else {
|
|
4813
|
+
deps.tracker.markFailed(issue.iid, errMsg, "phase_running" /* PhaseRunning */);
|
|
4814
|
+
}
|
|
4815
|
+
await safeComment(deps, issue.id, issueProgressComment(spec.name, "failed", shortErr));
|
|
4816
|
+
throw new AIExecutionError(spec.name, `Phase ${spec.name} failed: ${shortErr}`, {
|
|
4817
|
+
output: finalOutcome.error?.rawOutput ?? finalOutcome.output,
|
|
4818
|
+
exitCode: finalOutcome.exitCode ?? 1,
|
|
4819
|
+
isRetryable: finalOutcome.error?.isRetryable,
|
|
4820
|
+
wasActiveAtTimeout: wasActive
|
|
4821
|
+
});
|
|
4822
|
+
}
|
|
4823
|
+
deps.tracker.updateState(issue.iid, "phase_waiting" /* PhaseWaiting */, { currentPhase: spec.name });
|
|
4824
|
+
wtPlan.updatePhaseProgress(spec.name, "gate_waiting");
|
|
4825
|
+
deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "gate_waiting" });
|
|
4826
|
+
const gateEvent = spec.name === "uat" ? "uat:gateRequested" : "release:gateRequested";
|
|
4827
|
+
deps.eventBus.emitTyped(gateEvent, { issueIid: issue.iid });
|
|
4828
|
+
logger20.info("Async phase running (no awaitCompletion), pausing pipeline", {
|
|
4829
|
+
iid: issue.iid,
|
|
4830
|
+
phase: spec.name
|
|
4831
|
+
});
|
|
4832
|
+
return { paused: true };
|
|
4833
|
+
}
|
|
4834
|
+
handleGateRequest(ctx) {
|
|
4835
|
+
const { spec, issueCtx, deps, wtPlan } = ctx;
|
|
4836
|
+
const { issue } = issueCtx;
|
|
4837
|
+
deps.tracker.updateState(issue.iid, "phase_waiting" /* PhaseWaiting */, { currentPhase: spec.name });
|
|
4838
|
+
wtPlan.updatePhaseProgress(spec.name, "gate_waiting");
|
|
4839
|
+
deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "gate_waiting" });
|
|
4840
|
+
deps.eventBus.emitTyped("release:gateRequested", { issueIid: issue.iid });
|
|
4841
|
+
logger20.info("Phase requested gate, pausing", { iid: issue.iid, phase: spec.name });
|
|
4842
|
+
return { paused: true };
|
|
4843
|
+
}
|
|
4844
|
+
};
|
|
4845
|
+
|
|
4846
|
+
// src/orchestrator/strategies/VerifyFixStrategy.ts
|
|
4847
|
+
var logger21 = logger.child("VerifyFixStrategy");
|
|
4848
|
+
var VerifyFixStrategy = class {
|
|
4849
|
+
name = "verify-fix";
|
|
4850
|
+
shouldSkip() {
|
|
4851
|
+
return false;
|
|
4852
|
+
}
|
|
4853
|
+
async execute(ctx, hooks) {
|
|
4854
|
+
const { issueCtx, deps, wtGit, wtPlan, wtGitMap } = ctx;
|
|
4855
|
+
const { issue, phaseCtx, pipelineDef } = issueCtx;
|
|
4856
|
+
const maxIterations = deps.config.verifyFixLoop.maxIterations;
|
|
4857
|
+
const verifySpec = ctx.spec;
|
|
4858
|
+
const verifyPhaseIdx = pipelineDef.phases.findIndex((p) => p.name === verifySpec.name);
|
|
4859
|
+
const buildPhaseIdx = this.findPreviousAiPhaseIndex(pipelineDef.phases, verifyPhaseIdx);
|
|
4860
|
+
deps.eventBus.emitTyped("verify:loopStarted", {
|
|
4861
|
+
issueIid: issue.iid,
|
|
4862
|
+
maxIterations
|
|
4863
|
+
});
|
|
4864
|
+
logger21.info("Verify-fix loop started", {
|
|
4865
|
+
iid: issue.iid,
|
|
4866
|
+
maxIterations,
|
|
4867
|
+
buildPhaseIdx
|
|
4868
|
+
});
|
|
4869
|
+
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
4870
|
+
if (isShuttingDown()) throw new ServiceShutdownError();
|
|
4871
|
+
logger21.info("Verify-fix loop iteration", { iteration, maxIterations, iid: issue.iid });
|
|
4872
|
+
updateHooksForPhase(verifySpec, pipelineDef, issueCtx, wtPlan);
|
|
4873
|
+
const verifyRunner = resolveVerifyRunner(deps);
|
|
4874
|
+
const verifyPhase = createPhase("verify", verifyRunner, wtGit, wtPlan, deps.config);
|
|
4875
|
+
if (wtGitMap) verifyPhase.setWtGitMap(wtGitMap);
|
|
4876
|
+
let verifyOutcome;
|
|
4877
|
+
try {
|
|
4878
|
+
verifyOutcome = await runPhaseWithLifecycle(
|
|
4879
|
+
verifyPhase,
|
|
4880
|
+
phaseCtx,
|
|
4881
|
+
verifySpec,
|
|
4882
|
+
issueCtx,
|
|
4883
|
+
deps,
|
|
4884
|
+
wtGit,
|
|
4885
|
+
wtPlan,
|
|
4886
|
+
wtGitMap,
|
|
4887
|
+
hooks
|
|
4888
|
+
);
|
|
4889
|
+
} catch (err) {
|
|
4890
|
+
logger21.warn("Verify phase execution failed", {
|
|
4891
|
+
iteration,
|
|
4892
|
+
iid: issue.iid,
|
|
4893
|
+
error: err.message
|
|
4894
|
+
});
|
|
4895
|
+
deps.eventBus.emitTyped("verify:iterationComplete", {
|
|
4896
|
+
issueIid: issue.iid,
|
|
4897
|
+
iteration,
|
|
4898
|
+
passed: false,
|
|
4899
|
+
failures: ["AI runner execution failed"]
|
|
4900
|
+
});
|
|
4901
|
+
if (iteration === maxIterations) throw err;
|
|
4902
|
+
if (buildPhaseIdx >= 0) {
|
|
4903
|
+
await this.executeBuildFix(issueCtx, deps, wtGit, wtPlan, buildPhaseIdx, {
|
|
4904
|
+
iteration,
|
|
4905
|
+
verifyFailures: ["AI runner execution failed: " + err.message],
|
|
4906
|
+
rawReport: ""
|
|
4907
|
+
}, wtGitMap, hooks);
|
|
4908
|
+
}
|
|
4909
|
+
continue;
|
|
4910
|
+
}
|
|
4911
|
+
const report = verifyOutcome.data?.verifyReport;
|
|
4912
|
+
const passed = report ? report.passed : true;
|
|
4913
|
+
deps.eventBus.emitTyped("verify:iterationComplete", {
|
|
4914
|
+
issueIid: issue.iid,
|
|
4915
|
+
iteration,
|
|
4916
|
+
passed,
|
|
4917
|
+
failures: report?.failureReasons
|
|
4918
|
+
});
|
|
4919
|
+
if (passed) {
|
|
4920
|
+
logger21.info("Verify-fix loop passed", { iteration, iid: issue.iid });
|
|
4921
|
+
return { paused: false };
|
|
4922
|
+
}
|
|
4923
|
+
logger21.info("Verify failed, issues found", {
|
|
4924
|
+
iteration,
|
|
4925
|
+
iid: issue.iid,
|
|
4926
|
+
failures: report?.failureReasons,
|
|
4927
|
+
todolistStats: report?.todolistStats
|
|
4928
|
+
});
|
|
4929
|
+
if (iteration === maxIterations) {
|
|
4930
|
+
deps.eventBus.emitTyped("verify:loopExhausted", {
|
|
4931
|
+
issueIid: issue.iid,
|
|
4932
|
+
totalIterations: iteration,
|
|
4933
|
+
failures: report?.failureReasons ?? []
|
|
4934
|
+
});
|
|
4935
|
+
const failMsg = `Verify-fix loop exhausted after ${maxIterations} iterations. Remaining issues: ${report?.failureReasons?.join("; ") ?? "unknown"}`;
|
|
4936
|
+
logger21.warn(failMsg, { iid: issue.iid });
|
|
4937
|
+
throw new AIExecutionError("verify", failMsg, {
|
|
4938
|
+
output: report?.rawReport ?? "",
|
|
4939
|
+
exitCode: 0
|
|
4940
|
+
});
|
|
4941
|
+
}
|
|
4942
|
+
if (buildPhaseIdx >= 0) {
|
|
4943
|
+
await this.executeBuildFix(issueCtx, deps, wtGit, wtPlan, buildPhaseIdx, {
|
|
4944
|
+
iteration,
|
|
4945
|
+
verifyFailures: report?.failureReasons ?? [],
|
|
4946
|
+
rawReport: report?.rawReport ?? ""
|
|
4947
|
+
}, wtGitMap, hooks);
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
return { paused: false };
|
|
4951
|
+
}
|
|
4952
|
+
async executeBuildFix(ctx, deps, wtGit, wtPlan, buildPhaseIdx, fixContext, wtGitMap, hooks) {
|
|
4953
|
+
const { issue, phaseCtx, pipelineDef } = ctx;
|
|
4954
|
+
const buildSpec = pipelineDef.phases[buildPhaseIdx];
|
|
4955
|
+
logger21.info("Looping back to build for fix", {
|
|
4956
|
+
iteration: fixContext.iteration,
|
|
4957
|
+
iid: issue.iid,
|
|
4958
|
+
failures: fixContext.verifyFailures
|
|
4959
|
+
});
|
|
4960
|
+
phaseCtx.fixContext = fixContext;
|
|
4961
|
+
try {
|
|
4962
|
+
updateHooksForPhase(buildSpec, pipelineDef, ctx, wtPlan);
|
|
4963
|
+
const buildPhase = createPhase("build", deps.aiRunner, wtGit, wtPlan, deps.config);
|
|
4964
|
+
if (wtGitMap) buildPhase.setWtGitMap(wtGitMap);
|
|
4965
|
+
await runPhaseWithLifecycle(
|
|
4966
|
+
buildPhase,
|
|
4967
|
+
phaseCtx,
|
|
4968
|
+
buildSpec,
|
|
4969
|
+
ctx,
|
|
4970
|
+
deps,
|
|
4971
|
+
wtGit,
|
|
4972
|
+
wtPlan,
|
|
4973
|
+
wtGitMap,
|
|
4974
|
+
hooks
|
|
4975
|
+
);
|
|
4976
|
+
} finally {
|
|
4977
|
+
delete phaseCtx.fixContext;
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
findPreviousAiPhaseIndex(phases, currentIdx) {
|
|
4981
|
+
for (let j = currentIdx - 1; j >= 0; j--) {
|
|
4982
|
+
if (phases[j].kind === "ai") return j;
|
|
4983
|
+
}
|
|
4984
|
+
return -1;
|
|
4985
|
+
}
|
|
4986
|
+
};
|
|
4987
|
+
|
|
4988
|
+
// src/orchestrator/strategies/index.ts
|
|
4989
|
+
var gateStrategy = new GateStrategy();
|
|
4990
|
+
var aiStrategy = new AiPhaseStrategy();
|
|
4991
|
+
var verifyFixStrategy = new VerifyFixStrategy();
|
|
4992
|
+
function resolveStrategy(spec, config) {
|
|
4993
|
+
if (spec.kind === "gate") {
|
|
4994
|
+
return gateStrategy;
|
|
4995
|
+
}
|
|
4996
|
+
if (spec.name === "verify" && config.verifyFixLoop.enabled) {
|
|
4997
|
+
return verifyFixStrategy;
|
|
4998
|
+
}
|
|
4999
|
+
return aiStrategy;
|
|
5000
|
+
}
|
|
5001
|
+
|
|
5002
|
+
// src/orchestrator/steps/PhaseLoopStep.ts
|
|
5003
|
+
var logger22 = logger.child("PhaseLoopStep");
|
|
5004
|
+
async function runPhaseWithLifecycle(phase, phaseCtx, spec, ctx, deps, wtGit, wtPlan, wtGitMap, hook) {
|
|
5005
|
+
const lifecycleHook = hook ?? new DefaultLifecycleHook();
|
|
5006
|
+
const execCtx = {
|
|
5007
|
+
spec,
|
|
5008
|
+
issueCtx: ctx,
|
|
5009
|
+
deps,
|
|
5010
|
+
wtGit,
|
|
5011
|
+
wtPlan,
|
|
5012
|
+
wtGitMap,
|
|
5013
|
+
phase,
|
|
5014
|
+
displayId: ctx.issue.iid
|
|
5015
|
+
};
|
|
5016
|
+
await lifecycleHook.beforePhase(execCtx);
|
|
5017
|
+
if (lifecycleHook.beforeExecute) {
|
|
5018
|
+
await lifecycleHook.beforeExecute(execCtx);
|
|
5019
|
+
}
|
|
5020
|
+
const callbacks = createCallbacksFromHook(lifecycleHook, execCtx);
|
|
5021
|
+
const outcome = await phase.run(phaseCtx, callbacks);
|
|
5022
|
+
if (outcome.sessionId) wtPlan.updatePhaseSessionId(spec.name, outcome.sessionId);
|
|
5023
|
+
const finalOutcome = lifecycleHook.afterExecute ? await lifecycleHook.afterExecute(execCtx, outcome) : outcome;
|
|
5024
|
+
if (finalOutcome.status === "completed") {
|
|
5025
|
+
await lifecycleHook.afterPhase(execCtx, finalOutcome);
|
|
5026
|
+
return finalOutcome;
|
|
5027
|
+
}
|
|
5028
|
+
if (finalOutcome.status === "running") {
|
|
5029
|
+
return finalOutcome;
|
|
5030
|
+
}
|
|
5031
|
+
const errMsg = finalOutcome.error?.message ?? "Unknown error";
|
|
5032
|
+
const phaseError = {
|
|
5033
|
+
message: errMsg,
|
|
5034
|
+
isRetryable: finalOutcome.error?.isRetryable ?? true,
|
|
5035
|
+
rawOutput: finalOutcome.error?.rawOutput ?? finalOutcome.output,
|
|
5036
|
+
wasActiveAtTimeout: finalOutcome.error?.wasActiveAtTimeout ?? false
|
|
5037
|
+
};
|
|
5038
|
+
await lifecycleHook.onError(execCtx, phaseError);
|
|
5039
|
+
const shortErr = errMsg.slice(0, 200);
|
|
4095
5040
|
throw new AIExecutionError(spec.name, `Phase ${spec.name} failed: ${shortErr}`, {
|
|
4096
|
-
output:
|
|
4097
|
-
exitCode:
|
|
4098
|
-
isRetryable:
|
|
4099
|
-
wasActiveAtTimeout:
|
|
5041
|
+
output: finalOutcome.error?.rawOutput ?? finalOutcome.output,
|
|
5042
|
+
exitCode: finalOutcome.exitCode ?? 1,
|
|
5043
|
+
isRetryable: finalOutcome.error?.isRetryable,
|
|
5044
|
+
wasActiveAtTimeout: finalOutcome.error?.wasActiveAtTimeout ?? false
|
|
4100
5045
|
});
|
|
4101
5046
|
}
|
|
4102
5047
|
async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
@@ -4120,15 +5065,15 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
|
4120
5065
|
if (skippedDeployPhase && !phaseCtx.ports) {
|
|
4121
5066
|
const existingPorts = deps.getPortsForIssue(issue.iid);
|
|
4122
5067
|
if (existingPorts && deps.isPreviewRunning(issue.iid)) {
|
|
4123
|
-
|
|
5068
|
+
logger22.info("Restored preview ports from allocator", { iid: issue.iid, ...existingPorts });
|
|
4124
5069
|
phaseCtx.ports = existingPorts;
|
|
4125
5070
|
ctx.wtCtx.ports = existingPorts;
|
|
4126
5071
|
serversStarted = true;
|
|
4127
5072
|
} else {
|
|
4128
5073
|
if (existingPorts) {
|
|
4129
|
-
|
|
5074
|
+
logger22.info("Ports allocated but servers not running, restarting", { iid: issue.iid });
|
|
4130
5075
|
} else {
|
|
4131
|
-
|
|
5076
|
+
logger22.info("Restarting preview servers for resumed pipeline", { iid: issue.iid });
|
|
4132
5077
|
}
|
|
4133
5078
|
const ports = await deps.startPreviewServers(ctx.wtCtx, issue);
|
|
4134
5079
|
if (ports) {
|
|
@@ -4140,93 +5085,25 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
|
4140
5085
|
}
|
|
4141
5086
|
}
|
|
4142
5087
|
if (startIdx > 0) {
|
|
4143
|
-
|
|
4144
|
-
if (currentProgress) {
|
|
4145
|
-
let patched = false;
|
|
4146
|
-
for (let i = 0; i < startIdx; i++) {
|
|
4147
|
-
const prevSpec = pipelineDef.phases[i];
|
|
4148
|
-
const pp = currentProgress.phases[prevSpec.name];
|
|
4149
|
-
if (pp && pp.status !== "completed") {
|
|
4150
|
-
logger15.warn("Fixing stale phase progress", {
|
|
4151
|
-
iid: issue.iid,
|
|
4152
|
-
phase: prevSpec.name,
|
|
4153
|
-
was: pp.status,
|
|
4154
|
-
now: "completed"
|
|
4155
|
-
});
|
|
4156
|
-
pp.status = "completed";
|
|
4157
|
-
if (!pp.completedAt) {
|
|
4158
|
-
pp.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4159
|
-
}
|
|
4160
|
-
patched = true;
|
|
4161
|
-
}
|
|
4162
|
-
}
|
|
4163
|
-
if (patched) {
|
|
4164
|
-
wtPlan.writeProgress(currentProgress);
|
|
4165
|
-
}
|
|
4166
|
-
}
|
|
4167
|
-
if (record.phaseProgress) {
|
|
4168
|
-
for (let i = 0; i < startIdx; i++) {
|
|
4169
|
-
const prevSpec = pipelineDef.phases[i];
|
|
4170
|
-
const tp = record.phaseProgress[prevSpec.name];
|
|
4171
|
-
if (tp && tp.status !== "completed") {
|
|
4172
|
-
deps.tracker.updatePhaseProgress(issue.iid, prevSpec.name, {
|
|
4173
|
-
status: "completed",
|
|
4174
|
-
completedAt: tp.completedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
4175
|
-
});
|
|
4176
|
-
}
|
|
4177
|
-
}
|
|
4178
|
-
}
|
|
5088
|
+
healStaleProgress(ctx, deps, wtPlan, startIdx);
|
|
4179
5089
|
}
|
|
5090
|
+
const hooks = new DefaultLifecycleHook();
|
|
4180
5091
|
for (let i = startIdx; i < pipelineDef.phases.length; i++) {
|
|
4181
|
-
if (isShuttingDown())
|
|
4182
|
-
throw new ServiceShutdownError();
|
|
4183
|
-
}
|
|
5092
|
+
if (isShuttingDown()) throw new ServiceShutdownError();
|
|
4184
5093
|
const spec = pipelineDef.phases[i];
|
|
4185
5094
|
const pendingAction = deps.consumePendingAction?.(issue.iid);
|
|
4186
|
-
if (pendingAction)
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
}
|
|
4199
|
-
wtPlan.updatePhaseProgress(spec.name, "completed");
|
|
4200
|
-
deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
|
|
4201
|
-
status: "completed",
|
|
4202
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4203
|
-
});
|
|
4204
|
-
try {
|
|
4205
|
-
await deps.gongfeng.createIssueNote(
|
|
4206
|
-
issue.id,
|
|
4207
|
-
t("orchestrator.autoApproveComment")
|
|
4208
|
-
);
|
|
4209
|
-
} catch {
|
|
4210
|
-
}
|
|
4211
|
-
continue;
|
|
4212
|
-
}
|
|
4213
|
-
deps.tracker.updateState(issue.iid, spec.startState, { currentPhase: spec.name });
|
|
4214
|
-
wtPlan.updatePhaseProgress(spec.name, "in_progress");
|
|
4215
|
-
deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
|
|
4216
|
-
status: "in_progress",
|
|
4217
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4218
|
-
});
|
|
4219
|
-
deps.eventBus.emitTyped("review:requested", { issueIid: issue.iid });
|
|
4220
|
-
logger15.info("Review gate reached, pausing", { iid: issue.iid });
|
|
4221
|
-
return { serversStarted, paused: true };
|
|
4222
|
-
}
|
|
4223
|
-
if (spec.name === "verify" && deps.config.verifyFixLoop.enabled) {
|
|
4224
|
-
const buildIdx = findPreviousAiPhaseIndex(pipelineDef.phases, i);
|
|
4225
|
-
await executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, i, buildIdx, wtGitMap);
|
|
4226
|
-
continue;
|
|
4227
|
-
}
|
|
4228
|
-
if (spec.name === "uat" && !isE2eEnabledForIssue(issue.iid, deps.tracker, deps.config)) {
|
|
4229
|
-
logger15.info("UAT phase skipped (E2E not enabled for this issue)", { iid: issue.iid });
|
|
5095
|
+
if (pendingAction) throw new PhaseAbortedError(spec.name, pendingAction);
|
|
5096
|
+
const strategy = resolveStrategy(spec, deps.config);
|
|
5097
|
+
const execCtx = {
|
|
5098
|
+
spec,
|
|
5099
|
+
issueCtx: ctx,
|
|
5100
|
+
deps,
|
|
5101
|
+
wtGit,
|
|
5102
|
+
wtPlan,
|
|
5103
|
+
wtGitMap,
|
|
5104
|
+
displayId: issue.iid
|
|
5105
|
+
};
|
|
5106
|
+
if (strategy.shouldSkip(execCtx)) {
|
|
4230
5107
|
deps.tracker.updateState(issue.iid, spec.doneState, { currentPhase: spec.name });
|
|
4231
5108
|
wtPlan.updatePhaseProgress(spec.name, "completed");
|
|
4232
5109
|
deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
|
|
@@ -4235,48 +5112,12 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
|
4235
5112
|
});
|
|
4236
5113
|
continue;
|
|
4237
5114
|
}
|
|
4238
|
-
const
|
|
4239
|
-
if (
|
|
4240
|
-
|
|
4241
|
-
logger15.info("UAT phase starting", { iid: issue.iid, runner: runnerName });
|
|
4242
|
-
}
|
|
4243
|
-
const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config);
|
|
4244
|
-
if (wtGitMap) {
|
|
4245
|
-
phase.setWtGitMap(wtGitMap);
|
|
4246
|
-
}
|
|
4247
|
-
const outcome = await runPhaseWithLifecycle(
|
|
4248
|
-
phase,
|
|
4249
|
-
phaseCtx,
|
|
4250
|
-
spec,
|
|
4251
|
-
ctx,
|
|
4252
|
-
deps,
|
|
4253
|
-
wtGit,
|
|
4254
|
-
wtPlan,
|
|
4255
|
-
wtGitMap
|
|
4256
|
-
);
|
|
4257
|
-
if (outcome.status === "running") {
|
|
4258
|
-
if (outcome.awaitCompletion) {
|
|
4259
|
-
logger15.info("Async phase running, awaiting completion", { iid: issue.iid, phase: spec.name });
|
|
4260
|
-
const finalOutcome = await awaitAsyncPhase(outcome, spec, ctx, deps, wtGit, wtPlan, wtGitMap);
|
|
4261
|
-
if (finalOutcome.status === "completed") {
|
|
4262
|
-
continue;
|
|
4263
|
-
}
|
|
4264
|
-
}
|
|
4265
|
-
deps.tracker.updateState(issue.iid, "phase_waiting" /* PhaseWaiting */, { currentPhase: spec.name });
|
|
4266
|
-
wtPlan.updatePhaseProgress(spec.name, "gate_waiting");
|
|
4267
|
-
deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "gate_waiting" });
|
|
4268
|
-
const gateEvent = spec.name === "uat" ? "uat:gateRequested" : "release:gateRequested";
|
|
4269
|
-
deps.eventBus.emitTyped(gateEvent, { issueIid: issue.iid });
|
|
4270
|
-
logger15.info("Async phase running (no awaitCompletion), pausing pipeline", { iid: issue.iid, phase: spec.name });
|
|
4271
|
-
return { serversStarted, paused: true };
|
|
5115
|
+
const result = await strategy.execute(execCtx, hooks);
|
|
5116
|
+
if (result.paused) {
|
|
5117
|
+
return { serversStarted: serversStarted || !!result.serversStarted, paused: true };
|
|
4272
5118
|
}
|
|
4273
|
-
if (
|
|
4274
|
-
|
|
4275
|
-
wtPlan.updatePhaseProgress(spec.name, "gate_waiting");
|
|
4276
|
-
deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "gate_waiting" });
|
|
4277
|
-
deps.eventBus.emitTyped("release:gateRequested", { issueIid: issue.iid });
|
|
4278
|
-
logger15.info("Phase requested gate, pausing", { iid: issue.iid, phase: spec.name });
|
|
4279
|
-
return { serversStarted, paused: true };
|
|
5119
|
+
if (result.serversStarted) {
|
|
5120
|
+
serversStarted = true;
|
|
4280
5121
|
}
|
|
4281
5122
|
if (needsDeployment && !serversStarted && lifecycleManager.shouldDeployPreview(spec.name)) {
|
|
4282
5123
|
const ports = await deps.startPreviewServers(ctx.wtCtx, issue);
|
|
@@ -4289,189 +5130,48 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
|
4289
5130
|
}
|
|
4290
5131
|
return { serversStarted, paused: false };
|
|
4291
5132
|
}
|
|
4292
|
-
|
|
4293
|
-
const { issue } = ctx;
|
|
4294
|
-
const
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
return finalOutcome;
|
|
4313
|
-
}
|
|
4314
|
-
const errMsg = finalOutcome.error?.message ?? "Unknown error";
|
|
4315
|
-
const shortErr = errMsg.slice(0, 200);
|
|
4316
|
-
const wasActive = finalOutcome.error?.wasActiveAtTimeout ?? false;
|
|
4317
|
-
wtPlan.updatePhaseProgress(spec.name, "failed", errMsg);
|
|
4318
|
-
deps.tracker.updatePhaseProgress(displayId, spec.name, { status: "failed" });
|
|
4319
|
-
if (wasActive) {
|
|
4320
|
-
deps.tracker.markFailedSoft(displayId, errMsg, "phase_running" /* PhaseRunning */);
|
|
4321
|
-
} else {
|
|
4322
|
-
deps.tracker.markFailed(displayId, errMsg, "phase_running" /* PhaseRunning */);
|
|
4323
|
-
}
|
|
4324
|
-
await safeComment(deps, issue.id, issueProgressComment(spec.name, "failed", shortErr));
|
|
4325
|
-
throw new AIExecutionError(spec.name, `Phase ${spec.name} failed: ${shortErr}`, {
|
|
4326
|
-
output: finalOutcome.error?.rawOutput ?? finalOutcome.output,
|
|
4327
|
-
exitCode: finalOutcome.exitCode ?? 1,
|
|
4328
|
-
isRetryable: finalOutcome.error?.isRetryable,
|
|
4329
|
-
wasActiveAtTimeout: wasActive
|
|
4330
|
-
});
|
|
4331
|
-
}
|
|
4332
|
-
function findPreviousAiPhaseIndex(phases, currentIdx) {
|
|
4333
|
-
for (let j = currentIdx - 1; j >= 0; j--) {
|
|
4334
|
-
if (phases[j].kind === "ai") return j;
|
|
4335
|
-
}
|
|
4336
|
-
return -1;
|
|
4337
|
-
}
|
|
4338
|
-
async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, buildPhaseIdx, wtGitMap) {
|
|
4339
|
-
const { issue, lifecycleManager, phaseCtx } = ctx;
|
|
4340
|
-
const maxIterations = deps.config.verifyFixLoop.maxIterations;
|
|
4341
|
-
const verifySpec = ctx.pipelineDef.phases[verifyPhaseIdx];
|
|
4342
|
-
deps.eventBus.emitTyped("verify:loopStarted", {
|
|
4343
|
-
issueIid: issue.iid,
|
|
4344
|
-
maxIterations
|
|
4345
|
-
});
|
|
4346
|
-
logger15.info("Verify-fix loop started", {
|
|
4347
|
-
iid: issue.iid,
|
|
4348
|
-
maxIterations,
|
|
4349
|
-
buildPhaseIdx
|
|
4350
|
-
});
|
|
4351
|
-
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
4352
|
-
if (isShuttingDown()) {
|
|
4353
|
-
throw new ServiceShutdownError();
|
|
4354
|
-
}
|
|
4355
|
-
logger15.info("Verify-fix loop iteration", {
|
|
4356
|
-
iteration,
|
|
4357
|
-
maxIterations,
|
|
4358
|
-
iid: issue.iid
|
|
4359
|
-
});
|
|
4360
|
-
const verifyRunner = resolveVerifyRunner(deps);
|
|
4361
|
-
const verifyPhase = createPhase("verify", verifyRunner, wtGit, wtPlan, deps.config);
|
|
4362
|
-
if (wtGitMap) {
|
|
4363
|
-
verifyPhase.setWtGitMap(wtGitMap);
|
|
4364
|
-
}
|
|
4365
|
-
let verifyOutcome;
|
|
4366
|
-
try {
|
|
4367
|
-
verifyOutcome = await runPhaseWithLifecycle(
|
|
4368
|
-
verifyPhase,
|
|
4369
|
-
phaseCtx,
|
|
4370
|
-
verifySpec,
|
|
4371
|
-
ctx,
|
|
4372
|
-
deps,
|
|
4373
|
-
wtGit,
|
|
4374
|
-
wtPlan,
|
|
4375
|
-
wtGitMap
|
|
4376
|
-
);
|
|
4377
|
-
} catch (err) {
|
|
4378
|
-
logger15.warn("Verify phase execution failed", {
|
|
4379
|
-
iteration,
|
|
4380
|
-
iid: issue.iid,
|
|
4381
|
-
error: err.message
|
|
4382
|
-
});
|
|
4383
|
-
deps.eventBus.emitTyped("verify:iterationComplete", {
|
|
4384
|
-
issueIid: issue.iid,
|
|
4385
|
-
iteration,
|
|
4386
|
-
passed: false,
|
|
4387
|
-
failures: ["AI runner execution failed"]
|
|
4388
|
-
});
|
|
4389
|
-
if (iteration === maxIterations) {
|
|
4390
|
-
throw err;
|
|
4391
|
-
}
|
|
4392
|
-
if (buildPhaseIdx >= 0) {
|
|
4393
|
-
await executeBuildFix(ctx, deps, wtGit, wtPlan, buildPhaseIdx, {
|
|
4394
|
-
iteration,
|
|
4395
|
-
verifyFailures: ["AI runner execution failed: " + err.message],
|
|
4396
|
-
rawReport: ""
|
|
4397
|
-
}, wtGitMap);
|
|
5133
|
+
function healStaleProgress(ctx, deps, wtPlan, startIdx) {
|
|
5134
|
+
const { issue, pipelineDef, record } = ctx;
|
|
5135
|
+
const currentProgress = wtPlan.readProgress();
|
|
5136
|
+
if (currentProgress) {
|
|
5137
|
+
let patched = false;
|
|
5138
|
+
for (let i = 0; i < startIdx; i++) {
|
|
5139
|
+
const prevSpec = pipelineDef.phases[i];
|
|
5140
|
+
const pp = currentProgress.phases[prevSpec.name];
|
|
5141
|
+
if (pp && pp.status !== "completed") {
|
|
5142
|
+
logger22.warn("Fixing stale phase progress", {
|
|
5143
|
+
iid: issue.iid,
|
|
5144
|
+
phase: prevSpec.name,
|
|
5145
|
+
was: pp.status,
|
|
5146
|
+
now: "completed"
|
|
5147
|
+
});
|
|
5148
|
+
pp.status = "completed";
|
|
5149
|
+
if (!pp.completedAt) {
|
|
5150
|
+
pp.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5151
|
+
}
|
|
5152
|
+
patched = true;
|
|
4398
5153
|
}
|
|
4399
|
-
continue;
|
|
4400
|
-
}
|
|
4401
|
-
const report = verifyOutcome.data?.verifyReport;
|
|
4402
|
-
const passed = report ? report.passed : true;
|
|
4403
|
-
deps.eventBus.emitTyped("verify:iterationComplete", {
|
|
4404
|
-
issueIid: issue.iid,
|
|
4405
|
-
iteration,
|
|
4406
|
-
passed,
|
|
4407
|
-
failures: report?.failureReasons
|
|
4408
|
-
});
|
|
4409
|
-
if (passed) {
|
|
4410
|
-
logger15.info("Verify-fix loop passed", {
|
|
4411
|
-
iteration,
|
|
4412
|
-
iid: issue.iid
|
|
4413
|
-
});
|
|
4414
|
-
return;
|
|
4415
|
-
}
|
|
4416
|
-
logger15.info("Verify failed, issues found", {
|
|
4417
|
-
iteration,
|
|
4418
|
-
iid: issue.iid,
|
|
4419
|
-
failures: report?.failureReasons,
|
|
4420
|
-
todolistStats: report?.todolistStats
|
|
4421
|
-
});
|
|
4422
|
-
if (iteration === maxIterations) {
|
|
4423
|
-
deps.eventBus.emitTyped("verify:loopExhausted", {
|
|
4424
|
-
issueIid: issue.iid,
|
|
4425
|
-
totalIterations: iteration,
|
|
4426
|
-
failures: report?.failureReasons ?? []
|
|
4427
|
-
});
|
|
4428
|
-
const failMsg = `Verify-fix loop exhausted after ${maxIterations} iterations. Remaining issues: ${report?.failureReasons?.join("; ") ?? "unknown"}`;
|
|
4429
|
-
logger15.warn(failMsg, { iid: issue.iid });
|
|
4430
|
-
throw new AIExecutionError("verify", failMsg, {
|
|
4431
|
-
output: report?.rawReport ?? "",
|
|
4432
|
-
exitCode: 0
|
|
4433
|
-
});
|
|
4434
5154
|
}
|
|
4435
|
-
if (
|
|
4436
|
-
|
|
4437
|
-
iteration,
|
|
4438
|
-
verifyFailures: report?.failureReasons ?? [],
|
|
4439
|
-
rawReport: report?.rawReport ?? ""
|
|
4440
|
-
}, wtGitMap);
|
|
5155
|
+
if (patched) {
|
|
5156
|
+
wtPlan.writeProgress(currentProgress);
|
|
4441
5157
|
}
|
|
4442
5158
|
}
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
try {
|
|
4454
|
-
const buildPhase = createPhase("build", deps.aiRunner, wtGit, wtPlan, deps.config);
|
|
4455
|
-
if (wtGitMap) {
|
|
4456
|
-
buildPhase.setWtGitMap(wtGitMap);
|
|
5159
|
+
if (record.phaseProgress) {
|
|
5160
|
+
for (let i = 0; i < startIdx; i++) {
|
|
5161
|
+
const prevSpec = pipelineDef.phases[i];
|
|
5162
|
+
const tp = record.phaseProgress[prevSpec.name];
|
|
5163
|
+
if (tp && tp.status !== "completed") {
|
|
5164
|
+
deps.tracker.updatePhaseProgress(issue.iid, prevSpec.name, {
|
|
5165
|
+
status: "completed",
|
|
5166
|
+
completedAt: tp.completedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
5167
|
+
});
|
|
5168
|
+
}
|
|
4457
5169
|
}
|
|
4458
|
-
await runPhaseWithLifecycle(
|
|
4459
|
-
buildPhase,
|
|
4460
|
-
phaseCtx,
|
|
4461
|
-
buildSpec,
|
|
4462
|
-
ctx,
|
|
4463
|
-
deps,
|
|
4464
|
-
wtGit,
|
|
4465
|
-
wtPlan,
|
|
4466
|
-
wtGitMap
|
|
4467
|
-
);
|
|
4468
|
-
} finally {
|
|
4469
|
-
delete phaseCtx.fixContext;
|
|
4470
5170
|
}
|
|
4471
5171
|
}
|
|
4472
5172
|
|
|
4473
5173
|
// src/orchestrator/steps/CompletionStep.ts
|
|
4474
|
-
var
|
|
5174
|
+
var logger23 = logger.child("CompletionStep");
|
|
4475
5175
|
async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
|
|
4476
5176
|
const { issue, branchName, wtCtx } = ctx;
|
|
4477
5177
|
deps.emitProgress(issue.iid, "create_mr", t("orchestrator.createMrProgress"));
|
|
@@ -4503,7 +5203,7 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
|
|
|
4503
5203
|
mrIid: void 0
|
|
4504
5204
|
});
|
|
4505
5205
|
} catch (err) {
|
|
4506
|
-
|
|
5206
|
+
logger23.warn("Failed to publish E2E screenshots", {
|
|
4507
5207
|
iid: issue.iid,
|
|
4508
5208
|
error: err.message
|
|
4509
5209
|
});
|
|
@@ -4523,19 +5223,19 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
|
|
|
4523
5223
|
await deps.claimer.releaseClaim(issue.id, issue.iid, "completed");
|
|
4524
5224
|
}
|
|
4525
5225
|
if (phaseResult.serversStarted && deps.config.preview.keepAfterComplete) {
|
|
4526
|
-
|
|
5226
|
+
logger23.info("Preview servers kept running after completion", { iid: issue.iid });
|
|
4527
5227
|
} else {
|
|
4528
5228
|
deps.stopPreviewServers(issue.iid);
|
|
4529
5229
|
await deps.mainGitMutex.runExclusive(async () => {
|
|
4530
5230
|
if (wtCtx.workspace) {
|
|
4531
5231
|
await deps.workspaceManager.cleanupWorkspace(wtCtx.workspace);
|
|
4532
|
-
|
|
5232
|
+
logger23.info("Workspace cleaned up", { dir: wtCtx.workspace.workspaceRoot });
|
|
4533
5233
|
} else {
|
|
4534
5234
|
try {
|
|
4535
5235
|
await deps.mainGit.worktreeRemove(wtCtx.gitRootDir, true);
|
|
4536
|
-
|
|
5236
|
+
logger23.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
|
|
4537
5237
|
} catch (err) {
|
|
4538
|
-
|
|
5238
|
+
logger23.warn("Failed to cleanup worktree", {
|
|
4539
5239
|
dir: wtCtx.gitRootDir,
|
|
4540
5240
|
error: err.message
|
|
4541
5241
|
});
|
|
@@ -4543,16 +5243,16 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
|
|
|
4543
5243
|
}
|
|
4544
5244
|
});
|
|
4545
5245
|
}
|
|
4546
|
-
|
|
5246
|
+
logger23.info("Issue processing completed", { iid: issue.iid });
|
|
4547
5247
|
}
|
|
4548
5248
|
|
|
4549
5249
|
// src/orchestrator/steps/FailureHandler.ts
|
|
4550
|
-
var
|
|
5250
|
+
var logger24 = logger.child("FailureHandler");
|
|
4551
5251
|
async function handleFailure(err, issue, wtCtx, deps) {
|
|
4552
5252
|
const errorMsg = err.message;
|
|
4553
5253
|
const isRetryable = err instanceof AIExecutionError ? err.isRetryable : true;
|
|
4554
5254
|
const wasActiveAtTimeout = err instanceof AIExecutionError && err.wasActiveAtTimeout;
|
|
4555
|
-
|
|
5255
|
+
logger24.error("Issue processing failed", { iid: issue.iid, error: errorMsg, isRetryable, wasActiveAtTimeout });
|
|
4556
5256
|
metrics.incCounter("iaf_issues_failed_total");
|
|
4557
5257
|
const currentRecord = deps.tracker.get(issue.iid);
|
|
4558
5258
|
const failedAtState = currentRecord?.state || "pending" /* Pending */;
|
|
@@ -4565,11 +5265,11 @@ async function handleFailure(err, issue, wtCtx, deps) {
|
|
|
4565
5265
|
}
|
|
4566
5266
|
}
|
|
4567
5267
|
if (wasReset) {
|
|
4568
|
-
|
|
5268
|
+
logger24.info("Issue was reset during processing, skipping failure marking", { iid: issue.iid });
|
|
4569
5269
|
throw err;
|
|
4570
5270
|
}
|
|
4571
5271
|
if (failedAtState === "paused" /* Paused */) {
|
|
4572
|
-
|
|
5272
|
+
logger24.info("Issue was paused during processing, skipping failure handling", { iid: issue.iid });
|
|
4573
5273
|
throw err;
|
|
4574
5274
|
}
|
|
4575
5275
|
try {
|
|
@@ -4591,7 +5291,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
|
|
|
4591
5291
|
try {
|
|
4592
5292
|
await deps.claimer.releaseClaim(issue.id, issue.iid, "failed");
|
|
4593
5293
|
} catch (releaseErr) {
|
|
4594
|
-
|
|
5294
|
+
logger24.warn("Failed to release lock on failure", {
|
|
4595
5295
|
iid: issue.iid,
|
|
4596
5296
|
error: releaseErr.message
|
|
4597
5297
|
});
|
|
@@ -4599,7 +5299,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
|
|
|
4599
5299
|
}
|
|
4600
5300
|
deps.stopPreviewServers(issue.iid);
|
|
4601
5301
|
const preservedDirs = wtCtx.workspace ? [wtCtx.workspace.primary.gitRootDir, ...wtCtx.workspace.associates.map((a) => a.gitRootDir)] : [wtCtx.gitRootDir];
|
|
4602
|
-
|
|
5302
|
+
logger24.info("Worktree(s) preserved for debugging", {
|
|
4603
5303
|
primary: wtCtx.gitRootDir,
|
|
4604
5304
|
all: preservedDirs
|
|
4605
5305
|
});
|
|
@@ -4608,7 +5308,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
|
|
|
4608
5308
|
|
|
4609
5309
|
// src/orchestrator/PipelineOrchestrator.ts
|
|
4610
5310
|
var execFileAsync2 = promisify2(execFile2);
|
|
4611
|
-
var
|
|
5311
|
+
var logger25 = logger.child("PipelineOrchestrator");
|
|
4612
5312
|
var PipelineOrchestrator = class {
|
|
4613
5313
|
config;
|
|
4614
5314
|
gongfeng;
|
|
@@ -4638,7 +5338,7 @@ var PipelineOrchestrator = class {
|
|
|
4638
5338
|
setAIRunner(runner) {
|
|
4639
5339
|
this.aiRunner = runner;
|
|
4640
5340
|
this.conflictResolver = new ConflictResolver(runner);
|
|
4641
|
-
|
|
5341
|
+
logger25.info("AIRunner replaced via hot-reload");
|
|
4642
5342
|
}
|
|
4643
5343
|
constructor(config, gongfeng, git, aiRunner, tracker, supplementStore, mainGitMutex, eventBusInstance, wsConfig, tenantId, e2eAiRunner) {
|
|
4644
5344
|
this.config = config;
|
|
@@ -4656,14 +5356,14 @@ var PipelineOrchestrator = class {
|
|
|
4656
5356
|
this.pipelineDef = mode === "plan-mode" ? buildPlanModePipeline({ releaseEnabled: config.release.enabled, e2eEnabled: config.e2e.enabled }) : getPipelineDef(mode);
|
|
4657
5357
|
registerPipeline(this.pipelineDef);
|
|
4658
5358
|
this.lifecycleManager = createLifecycleManager(this.pipelineDef);
|
|
4659
|
-
|
|
5359
|
+
logger25.info("Pipeline mode resolved", { tenantId: this.tenantId, mode: this.pipelineDef.mode, aiMode: config.ai.mode });
|
|
4660
5360
|
this.portAllocator = new PortAllocator({
|
|
4661
5361
|
backendPortBase: config.e2e.backendPortBase,
|
|
4662
5362
|
frontendPortBase: config.e2e.frontendPortBase
|
|
4663
5363
|
});
|
|
4664
5364
|
this.devServerManager = new DevServerManager();
|
|
4665
5365
|
this.screenshotPublisher = new ScreenshotPublisher(gongfeng);
|
|
4666
|
-
this.effectiveWorktreeBaseDir = this.tenantId === "default" ? config.project.worktreeBaseDir :
|
|
5366
|
+
this.effectiveWorktreeBaseDir = this.tenantId === "default" ? config.project.worktreeBaseDir : path12.join(config.project.worktreeBaseDir, this.tenantId);
|
|
4667
5367
|
const effectiveWsConfig = wsConfig ?? buildSingleRepoWorkspace(config.project, config.gongfeng.projectPath);
|
|
4668
5368
|
this.workspaceManager = new WorkspaceManager({
|
|
4669
5369
|
wsConfig: effectiveWsConfig,
|
|
@@ -4672,7 +5372,7 @@ var PipelineOrchestrator = class {
|
|
|
4672
5372
|
mainGitMutex: this.mainGitMutex,
|
|
4673
5373
|
gongfengApiUrl: config.gongfeng.apiUrl
|
|
4674
5374
|
});
|
|
4675
|
-
|
|
5375
|
+
logger25.info("WorkspaceManager initialized", {
|
|
4676
5376
|
tenantId: this.tenantId,
|
|
4677
5377
|
primary: effectiveWsConfig.primary.name,
|
|
4678
5378
|
associates: effectiveWsConfig.associates.map((a) => a.name)
|
|
@@ -4693,7 +5393,7 @@ var PipelineOrchestrator = class {
|
|
|
4693
5393
|
this.claimer = claimer;
|
|
4694
5394
|
}
|
|
4695
5395
|
async cleanupStaleState() {
|
|
4696
|
-
|
|
5396
|
+
logger25.info("Cleaning up stale worktree state...");
|
|
4697
5397
|
let cleaned = 0;
|
|
4698
5398
|
const repoGitRoot = this.config.project.gitRootDir;
|
|
4699
5399
|
try {
|
|
@@ -4702,11 +5402,11 @@ var PipelineOrchestrator = class {
|
|
|
4702
5402
|
if (wtDir === repoGitRoot) continue;
|
|
4703
5403
|
if (!wtDir.includes("/issue-")) continue;
|
|
4704
5404
|
try {
|
|
4705
|
-
const gitFile =
|
|
5405
|
+
const gitFile = path12.join(wtDir, ".git");
|
|
4706
5406
|
try {
|
|
4707
|
-
await
|
|
5407
|
+
await fs11.access(gitFile);
|
|
4708
5408
|
} catch {
|
|
4709
|
-
|
|
5409
|
+
logger25.warn("Worktree corrupted (.git missing), force removing", { dir: wtDir });
|
|
4710
5410
|
await this.mainGit.worktreeRemove(wtDir, true).catch(() => {
|
|
4711
5411
|
});
|
|
4712
5412
|
await this.mainGit.worktreePrune();
|
|
@@ -4715,32 +5415,32 @@ var PipelineOrchestrator = class {
|
|
|
4715
5415
|
}
|
|
4716
5416
|
const wtGit = new GitOperations(wtDir);
|
|
4717
5417
|
if (await wtGit.isRebaseInProgress()) {
|
|
4718
|
-
|
|
5418
|
+
logger25.warn("Aborting residual rebase in worktree", { dir: wtDir });
|
|
4719
5419
|
await wtGit.rebaseAbort();
|
|
4720
5420
|
cleaned++;
|
|
4721
5421
|
}
|
|
4722
|
-
const indexLock =
|
|
5422
|
+
const indexLock = path12.join(wtDir, ".git", "index.lock");
|
|
4723
5423
|
try {
|
|
4724
|
-
await
|
|
4725
|
-
|
|
5424
|
+
await fs11.unlink(indexLock);
|
|
5425
|
+
logger25.warn("Removed stale index.lock", { path: indexLock });
|
|
4726
5426
|
cleaned++;
|
|
4727
5427
|
} catch {
|
|
4728
5428
|
}
|
|
4729
5429
|
} catch (err) {
|
|
4730
|
-
|
|
5430
|
+
logger25.warn("Failed to clean worktree state", { dir: wtDir, error: err.message });
|
|
4731
5431
|
}
|
|
4732
5432
|
}
|
|
4733
5433
|
} catch (err) {
|
|
4734
|
-
|
|
5434
|
+
logger25.warn("Failed to list worktrees for cleanup", { error: err.message });
|
|
4735
5435
|
}
|
|
4736
|
-
const mainIndexLock =
|
|
5436
|
+
const mainIndexLock = path12.join(repoGitRoot, ".git", "index.lock");
|
|
4737
5437
|
try {
|
|
4738
|
-
await
|
|
4739
|
-
|
|
5438
|
+
await fs11.unlink(mainIndexLock);
|
|
5439
|
+
logger25.warn("Removed stale main repo index.lock", { path: mainIndexLock });
|
|
4740
5440
|
cleaned++;
|
|
4741
5441
|
} catch {
|
|
4742
5442
|
}
|
|
4743
|
-
|
|
5443
|
+
logger25.info("Stale state cleanup complete", { cleaned });
|
|
4744
5444
|
}
|
|
4745
5445
|
/**
|
|
4746
5446
|
* 重启后清理幽灵端口分配。
|
|
@@ -4753,7 +5453,7 @@ var PipelineOrchestrator = class {
|
|
|
4753
5453
|
for (const record of this.tracker.getAll()) {
|
|
4754
5454
|
if (record.ports) {
|
|
4755
5455
|
const iid = getIid(record);
|
|
4756
|
-
|
|
5456
|
+
logger25.info("Clearing stale port allocation after restart", { iid, ports: record.ports });
|
|
4757
5457
|
this.tracker.updateState(iid, record.state, {
|
|
4758
5458
|
ports: void 0,
|
|
4759
5459
|
previewStartedAt: void 0
|
|
@@ -4798,20 +5498,20 @@ var PipelineOrchestrator = class {
|
|
|
4798
5498
|
}
|
|
4799
5499
|
try {
|
|
4800
5500
|
await this.mainGit.worktreeRemove(wtCtx.gitRootDir, true);
|
|
4801
|
-
|
|
5501
|
+
logger25.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
|
|
4802
5502
|
} catch (err) {
|
|
4803
|
-
|
|
5503
|
+
logger25.warn("Failed to cleanup worktree", { dir: wtCtx.gitRootDir, error: err.message });
|
|
4804
5504
|
}
|
|
4805
5505
|
}
|
|
4806
5506
|
async installDependencies(workDir) {
|
|
4807
|
-
|
|
5507
|
+
logger25.info("Installing dependencies in worktree", { workDir });
|
|
4808
5508
|
const knowledge = getProjectKnowledge() ?? KNOWLEDGE_DEFAULTS;
|
|
4809
5509
|
const pkgMgr = knowledge.toolchain.packageManager.toLowerCase();
|
|
4810
5510
|
const isNodeProject = ["npm", "pnpm", "yarn", "bun"].some((m) => pkgMgr.includes(m));
|
|
4811
5511
|
if (isNodeProject) {
|
|
4812
5512
|
const ready = await this.ensureNodeModules(workDir);
|
|
4813
5513
|
if (ready) {
|
|
4814
|
-
|
|
5514
|
+
logger25.info("node_modules ready \u2014 skipping install");
|
|
4815
5515
|
return;
|
|
4816
5516
|
}
|
|
4817
5517
|
}
|
|
@@ -4824,10 +5524,10 @@ var PipelineOrchestrator = class {
|
|
|
4824
5524
|
maxBuffer: 10 * 1024 * 1024,
|
|
4825
5525
|
timeout: 3e5
|
|
4826
5526
|
});
|
|
4827
|
-
|
|
5527
|
+
logger25.info("Dependencies installed");
|
|
4828
5528
|
} catch (err) {
|
|
4829
5529
|
if (fallbackCmd) {
|
|
4830
|
-
|
|
5530
|
+
logger25.warn(`${installCmd} failed, retrying with fallback command`, {
|
|
4831
5531
|
error: err.message
|
|
4832
5532
|
});
|
|
4833
5533
|
const [fallbackBin, ...fallbackArgs] = fallbackCmd.split(/\s+/);
|
|
@@ -4837,45 +5537,45 @@ var PipelineOrchestrator = class {
|
|
|
4837
5537
|
maxBuffer: 10 * 1024 * 1024,
|
|
4838
5538
|
timeout: 3e5
|
|
4839
5539
|
});
|
|
4840
|
-
|
|
5540
|
+
logger25.info("Dependencies installed (fallback)");
|
|
4841
5541
|
} catch (retryErr) {
|
|
4842
|
-
|
|
5542
|
+
logger25.warn("Fallback install also failed", {
|
|
4843
5543
|
error: retryErr.message
|
|
4844
5544
|
});
|
|
4845
5545
|
}
|
|
4846
5546
|
} else {
|
|
4847
|
-
|
|
5547
|
+
logger25.warn("Install failed, no fallback configured", {
|
|
4848
5548
|
error: err.message
|
|
4849
5549
|
});
|
|
4850
5550
|
}
|
|
4851
5551
|
}
|
|
4852
5552
|
}
|
|
4853
5553
|
async ensureNodeModules(workDir) {
|
|
4854
|
-
const targetBin =
|
|
5554
|
+
const targetBin = path12.join(workDir, "node_modules", ".bin");
|
|
4855
5555
|
try {
|
|
4856
|
-
await
|
|
4857
|
-
|
|
5556
|
+
await fs11.access(targetBin);
|
|
5557
|
+
logger25.info("node_modules already complete (has .bin/)");
|
|
4858
5558
|
return true;
|
|
4859
5559
|
} catch {
|
|
4860
5560
|
}
|
|
4861
|
-
const sourceNM =
|
|
4862
|
-
const targetNM =
|
|
5561
|
+
const sourceNM = path12.join(this.config.project.workDir, "node_modules");
|
|
5562
|
+
const targetNM = path12.join(workDir, "node_modules");
|
|
4863
5563
|
try {
|
|
4864
|
-
await
|
|
5564
|
+
await fs11.access(sourceNM);
|
|
4865
5565
|
} catch {
|
|
4866
|
-
|
|
5566
|
+
logger25.warn("Main repo node_modules not found, skipping seed", { sourceNM });
|
|
4867
5567
|
return false;
|
|
4868
5568
|
}
|
|
4869
|
-
|
|
5569
|
+
logger25.info("Seeding node_modules from main repo via reflink copy", { sourceNM, targetNM });
|
|
4870
5570
|
try {
|
|
4871
5571
|
await execFileAsync2("rm", ["-rf", targetNM], { timeout: 6e4 });
|
|
4872
5572
|
await execFileAsync2("cp", ["-a", "--reflink=auto", sourceNM, targetNM], {
|
|
4873
5573
|
timeout: 12e4
|
|
4874
5574
|
});
|
|
4875
|
-
|
|
5575
|
+
logger25.info("node_modules seeded from main repo");
|
|
4876
5576
|
return true;
|
|
4877
5577
|
} catch (err) {
|
|
4878
|
-
|
|
5578
|
+
logger25.warn("Failed to seed node_modules from main repo", {
|
|
4879
5579
|
error: err.message
|
|
4880
5580
|
});
|
|
4881
5581
|
return false;
|
|
@@ -4885,16 +5585,16 @@ var PipelineOrchestrator = class {
|
|
|
4885
5585
|
const record = this.tracker.get(issueIid);
|
|
4886
5586
|
if (!record) throw new IssueNotFoundError(issueIid);
|
|
4887
5587
|
const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
|
|
4888
|
-
|
|
5588
|
+
logger25.info("Restarting issue \u2014 cleaning context", { issueIid, branchName: record.branchName });
|
|
4889
5589
|
this.pendingActions.set(issueIid, "restart");
|
|
4890
5590
|
this.aiRunner.killByWorkDir(wtCtx.workDir);
|
|
4891
5591
|
this.e2eAiRunner?.killByWorkDir(wtCtx.workDir);
|
|
4892
5592
|
this.stopPreviewServers(issueIid);
|
|
4893
5593
|
try {
|
|
4894
5594
|
const deleted = await this.gongfeng.cleanupAgentNotes(getExternalId(record));
|
|
4895
|
-
|
|
5595
|
+
logger25.info("Agent notes cleaned up", { issueIid, deleted });
|
|
4896
5596
|
} catch (err) {
|
|
4897
|
-
|
|
5597
|
+
logger25.warn("Failed to cleanup agent notes", { issueIid, error: err.message });
|
|
4898
5598
|
}
|
|
4899
5599
|
await this.mainGitMutex.runExclusive(async () => {
|
|
4900
5600
|
await this.cleanupWorktree(wtCtx);
|
|
@@ -4911,19 +5611,19 @@ var PipelineOrchestrator = class {
|
|
|
4911
5611
|
await this.cleanupE2eOutputs(issueIid);
|
|
4912
5612
|
this.tracker.resetFull(issueIid);
|
|
4913
5613
|
this.pendingActions.delete(issueIid);
|
|
4914
|
-
|
|
5614
|
+
logger25.info("Issue restarted", { issueIid });
|
|
4915
5615
|
}
|
|
4916
5616
|
async cancelIssue(issueIid) {
|
|
4917
5617
|
const record = this.tracker.get(issueIid);
|
|
4918
5618
|
if (!record) throw new IssueNotFoundError(issueIid);
|
|
4919
5619
|
const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
|
|
4920
|
-
|
|
5620
|
+
logger25.info("Cancelling issue \u2014 cleaning all resources", { issueIid, branchName: record.branchName });
|
|
4921
5621
|
this.aiRunner.killByWorkDir(wtCtx.workDir);
|
|
4922
5622
|
this.stopPreviewServers(issueIid);
|
|
4923
5623
|
try {
|
|
4924
5624
|
await this.gongfeng.removeLabelsWithPrefix(getExternalId(record), "auto-finish");
|
|
4925
5625
|
} catch (err) {
|
|
4926
|
-
|
|
5626
|
+
logger25.warn("Failed to remove labels on cancel", { issueIid, error: err.message });
|
|
4927
5627
|
}
|
|
4928
5628
|
await this.mainGitMutex.runExclusive(async () => {
|
|
4929
5629
|
await this.cleanupWorktree(wtCtx);
|
|
@@ -4940,7 +5640,7 @@ var PipelineOrchestrator = class {
|
|
|
4940
5640
|
this.tracker.clearProcessingLock(issueIid);
|
|
4941
5641
|
this.tracker.updateState(issueIid, "skipped" /* Skipped */);
|
|
4942
5642
|
await this.cleanupE2eOutputs(issueIid);
|
|
4943
|
-
|
|
5643
|
+
logger25.info("Issue cancelled", { issueIid });
|
|
4944
5644
|
}
|
|
4945
5645
|
/**
|
|
4946
5646
|
* Remove the E2E output directory for an issue: {uatVendorDir}/outputs/issue-{iid}
|
|
@@ -4948,13 +5648,13 @@ var PipelineOrchestrator = class {
|
|
|
4948
5648
|
async cleanupE2eOutputs(issueIid) {
|
|
4949
5649
|
const vendorDir = this.config.e2e.uatVendorDir;
|
|
4950
5650
|
if (!vendorDir) return;
|
|
4951
|
-
const abs =
|
|
4952
|
-
const outputDir =
|
|
5651
|
+
const abs = path12.isAbsolute(vendorDir) ? vendorDir : path12.resolve(this.config.project.workDir, vendorDir);
|
|
5652
|
+
const outputDir = path12.join(abs, "outputs", `issue-${issueIid}`);
|
|
4953
5653
|
try {
|
|
4954
|
-
await
|
|
4955
|
-
|
|
5654
|
+
await fs11.rm(outputDir, { recursive: true, force: true });
|
|
5655
|
+
logger25.info("E2E outputs cleaned up", { issueIid, dir: outputDir });
|
|
4956
5656
|
} catch (err) {
|
|
4957
|
-
|
|
5657
|
+
logger25.warn("Failed to cleanup E2E outputs", { issueIid, dir: outputDir, error: err.message });
|
|
4958
5658
|
}
|
|
4959
5659
|
}
|
|
4960
5660
|
/**
|
|
@@ -4966,10 +5666,10 @@ var PipelineOrchestrator = class {
|
|
|
4966
5666
|
if (!this.workspaceManager) return;
|
|
4967
5667
|
const wsRoot = this.workspaceManager.getWorkspaceRoot(issueIid);
|
|
4968
5668
|
try {
|
|
4969
|
-
await
|
|
4970
|
-
|
|
5669
|
+
await fs11.rm(wsRoot, { recursive: true, force: true });
|
|
5670
|
+
logger25.info("Workspace root cleaned up", { issueIid, dir: wsRoot });
|
|
4971
5671
|
} catch (err) {
|
|
4972
|
-
|
|
5672
|
+
logger25.warn("Failed to cleanup workspace root", { issueIid, dir: wsRoot, error: err.message });
|
|
4973
5673
|
}
|
|
4974
5674
|
}
|
|
4975
5675
|
retryFromPhase(issueIid, phase) {
|
|
@@ -4985,7 +5685,7 @@ var PipelineOrchestrator = class {
|
|
|
4985
5685
|
this.aiRunner.killByWorkDir(wtCtx.workDir);
|
|
4986
5686
|
}
|
|
4987
5687
|
this.e2eAiRunner?.killByWorkDir(wtCtx.workDir);
|
|
4988
|
-
|
|
5688
|
+
logger25.info("Retrying issue from phase", { issueIid, phase });
|
|
4989
5689
|
const ok = this.tracker.resetToPhase(issueIid, phase, issueDef);
|
|
4990
5690
|
if (!ok) {
|
|
4991
5691
|
throw new InvalidPhaseError(phase);
|
|
@@ -5012,7 +5712,7 @@ var PipelineOrchestrator = class {
|
|
|
5012
5712
|
} else {
|
|
5013
5713
|
this.tracker.pauseIssue(issueIid, record.currentPhase ?? "");
|
|
5014
5714
|
}
|
|
5015
|
-
|
|
5715
|
+
logger25.info("Issue abort requested", { issueIid, state: record.state });
|
|
5016
5716
|
}
|
|
5017
5717
|
continueIssue(issueIid) {
|
|
5018
5718
|
const record = this.tracker.get(issueIid);
|
|
@@ -5022,7 +5722,7 @@ var PipelineOrchestrator = class {
|
|
|
5022
5722
|
}
|
|
5023
5723
|
const issueDef = this.getIssueSpecificPipelineDef(record);
|
|
5024
5724
|
this.tracker.resumeFromPause(issueIid, issueDef, false);
|
|
5025
|
-
|
|
5725
|
+
logger25.info("Issue continued from pause", { issueIid });
|
|
5026
5726
|
}
|
|
5027
5727
|
redoPhase(issueIid) {
|
|
5028
5728
|
const record = this.tracker.get(issueIid);
|
|
@@ -5066,7 +5766,7 @@ var PipelineOrchestrator = class {
|
|
|
5066
5766
|
}
|
|
5067
5767
|
this.eventBus.emitTyped("issue:redone", { issueIid });
|
|
5068
5768
|
}
|
|
5069
|
-
|
|
5769
|
+
logger25.info("Issue redo requested", { issueIid, state: record.state });
|
|
5070
5770
|
}
|
|
5071
5771
|
/**
|
|
5072
5772
|
* 处理中止/重做的共享逻辑:
|
|
@@ -5139,7 +5839,7 @@ var PipelineOrchestrator = class {
|
|
|
5139
5839
|
async _processIssueImpl(issue) {
|
|
5140
5840
|
const branchName = `${this.config.project.branchPrefix}-${issue.iid}`;
|
|
5141
5841
|
const wtCtx = this.computeWorktreeContext(issue.iid, branchName);
|
|
5142
|
-
|
|
5842
|
+
logger25.info("Processing issue", {
|
|
5143
5843
|
iid: issue.iid,
|
|
5144
5844
|
title: issue.title,
|
|
5145
5845
|
branchName,
|
|
@@ -5246,7 +5946,7 @@ var PipelineOrchestrator = class {
|
|
|
5246
5946
|
title,
|
|
5247
5947
|
description
|
|
5248
5948
|
});
|
|
5249
|
-
|
|
5949
|
+
logger25.info("Merge request created successfully", {
|
|
5250
5950
|
iid: issue.iid,
|
|
5251
5951
|
mrIid: mr.iid,
|
|
5252
5952
|
mrUrl: mr.web_url
|
|
@@ -5254,7 +5954,7 @@ var PipelineOrchestrator = class {
|
|
|
5254
5954
|
return { url: mr.web_url, iid: mr.iid };
|
|
5255
5955
|
} catch (err) {
|
|
5256
5956
|
const errorMsg = err.message;
|
|
5257
|
-
|
|
5957
|
+
logger25.warn("Failed to create merge request, trying to find existing one", {
|
|
5258
5958
|
iid: issue.iid,
|
|
5259
5959
|
error: errorMsg
|
|
5260
5960
|
});
|
|
@@ -5271,7 +5971,7 @@ var PipelineOrchestrator = class {
|
|
|
5271
5971
|
this.config.project.baseBranch
|
|
5272
5972
|
);
|
|
5273
5973
|
if (existing) {
|
|
5274
|
-
|
|
5974
|
+
logger25.info("Found existing merge request", {
|
|
5275
5975
|
iid: issueIid,
|
|
5276
5976
|
mrIid: existing.iid,
|
|
5277
5977
|
mrUrl: existing.web_url
|
|
@@ -5279,7 +5979,7 @@ var PipelineOrchestrator = class {
|
|
|
5279
5979
|
return { url: existing.web_url, iid: existing.iid };
|
|
5280
5980
|
}
|
|
5281
5981
|
} catch (findErr) {
|
|
5282
|
-
|
|
5982
|
+
logger25.warn("Failed to find existing merge request", {
|
|
5283
5983
|
iid: issueIid,
|
|
5284
5984
|
error: findErr.message
|
|
5285
5985
|
});
|
|
@@ -5324,7 +6024,7 @@ var PipelineOrchestrator = class {
|
|
|
5324
6024
|
});
|
|
5325
6025
|
return ports;
|
|
5326
6026
|
} catch (err) {
|
|
5327
|
-
|
|
6027
|
+
logger25.error("Failed to start preview servers", {
|
|
5328
6028
|
iid: issue.iid,
|
|
5329
6029
|
error: err.message
|
|
5330
6030
|
});
|
|
@@ -5359,7 +6059,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5359
6059
|
await this.mainGitMutex.runExclusive(async () => {
|
|
5360
6060
|
await this.cleanupWorktree(wtCtx);
|
|
5361
6061
|
});
|
|
5362
|
-
|
|
6062
|
+
logger25.info("Preview stopped and worktree cleaned", { iid: issueIid });
|
|
5363
6063
|
}
|
|
5364
6064
|
async markDeployed(issueIid) {
|
|
5365
6065
|
const record = this.tracker.get(issueIid);
|
|
@@ -5376,7 +6076,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5376
6076
|
try {
|
|
5377
6077
|
await this.gongfeng.closeIssue(externalId);
|
|
5378
6078
|
} catch (err) {
|
|
5379
|
-
|
|
6079
|
+
logger25.warn("Failed to close issue on Gongfeng", { iid: issueIid, error: err.message });
|
|
5380
6080
|
}
|
|
5381
6081
|
try {
|
|
5382
6082
|
const issue = await this.gongfeng.getIssueDetail(externalId);
|
|
@@ -5384,10 +6084,10 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5384
6084
|
labels.push("auto-finish:deployed");
|
|
5385
6085
|
await this.gongfeng.updateIssueLabels(externalId, labels);
|
|
5386
6086
|
} catch (err) {
|
|
5387
|
-
|
|
6087
|
+
logger25.warn("Failed to update labels", { iid: issueIid, error: err.message });
|
|
5388
6088
|
}
|
|
5389
6089
|
this.tracker.updateState(issueIid, "deployed" /* Deployed */);
|
|
5390
|
-
|
|
6090
|
+
logger25.info("Issue marked as deployed", { iid: issueIid });
|
|
5391
6091
|
}
|
|
5392
6092
|
async restartPreview(issueIid) {
|
|
5393
6093
|
const record = this.tracker.get(issueIid);
|
|
@@ -5414,7 +6114,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5414
6114
|
throw err;
|
|
5415
6115
|
}
|
|
5416
6116
|
const url = this.buildPreviewUrl(issueIid);
|
|
5417
|
-
|
|
6117
|
+
logger25.info("Preview restarted", { iid: issueIid, url });
|
|
5418
6118
|
return url;
|
|
5419
6119
|
}
|
|
5420
6120
|
getPreviewHost() {
|
|
@@ -5447,7 +6147,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5447
6147
|
if (!record) throw new IssueNotFoundError(issueIid);
|
|
5448
6148
|
const baseBranch = this.config.project.baseBranch;
|
|
5449
6149
|
const branchName = record.branchName;
|
|
5450
|
-
|
|
6150
|
+
logger25.info("Starting conflict resolution", { issueIid, branchName, baseBranch });
|
|
5451
6151
|
this.tracker.updateState(issueIid, "resolving_conflict" /* ResolvingConflict */);
|
|
5452
6152
|
this.eventBus.emitTyped("conflict:started", { issueIid });
|
|
5453
6153
|
try {
|
|
@@ -5480,7 +6180,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5480
6180
|
});
|
|
5481
6181
|
}
|
|
5482
6182
|
});
|
|
5483
|
-
|
|
6183
|
+
logger25.info("Running verification after conflict resolution", { issueIid });
|
|
5484
6184
|
const wtPlan = new PlanPersistence(wtCtx.workDir, issueIid);
|
|
5485
6185
|
wtPlan.ensureDir();
|
|
5486
6186
|
const verifyPhase = createPhase("verify", this.aiRunner, wtGit, wtPlan, this.config);
|
|
@@ -5518,10 +6218,10 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5518
6218
|
} catch {
|
|
5519
6219
|
}
|
|
5520
6220
|
await this.commentOnMr(record.mrUrl, t("conflict.mrResolvedComment"));
|
|
5521
|
-
|
|
6221
|
+
logger25.info("Conflict resolution completed", { issueIid });
|
|
5522
6222
|
} catch (err) {
|
|
5523
6223
|
const errorMsg = err.message;
|
|
5524
|
-
|
|
6224
|
+
logger25.error("Conflict resolution failed", { issueIid, error: errorMsg });
|
|
5525
6225
|
try {
|
|
5526
6226
|
const wtGit = new GitOperations(wtCtx.gitRootDir);
|
|
5527
6227
|
if (await wtGit.isRebaseInProgress()) {
|
|
@@ -5551,7 +6251,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5551
6251
|
try {
|
|
5552
6252
|
await this.gongfeng.createMergeRequestNote(mrIid, body);
|
|
5553
6253
|
} catch (err) {
|
|
5554
|
-
|
|
6254
|
+
logger25.warn("Failed to comment on MR", { mrIid, error: err.message });
|
|
5555
6255
|
}
|
|
5556
6256
|
}
|
|
5557
6257
|
};
|
|
@@ -5627,7 +6327,7 @@ ${questions}
|
|
|
5627
6327
|
}
|
|
5628
6328
|
|
|
5629
6329
|
// src/services/BrainstormService.ts
|
|
5630
|
-
var
|
|
6330
|
+
var logger26 = logger.child("Brainstorm");
|
|
5631
6331
|
function agentConfigToAIConfig(agentCfg, timeoutMs) {
|
|
5632
6332
|
return {
|
|
5633
6333
|
mode: agentCfg.mode,
|
|
@@ -5663,7 +6363,7 @@ var BrainstormService = class {
|
|
|
5663
6363
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5664
6364
|
};
|
|
5665
6365
|
this.sessions.set(session.id, session);
|
|
5666
|
-
|
|
6366
|
+
logger26.info("Created brainstorm session", { sessionId: session.id });
|
|
5667
6367
|
return session;
|
|
5668
6368
|
}
|
|
5669
6369
|
getSession(id) {
|
|
@@ -5672,7 +6372,7 @@ var BrainstormService = class {
|
|
|
5672
6372
|
async generate(sessionId, onEvent) {
|
|
5673
6373
|
const session = this.requireSession(sessionId);
|
|
5674
6374
|
session.status = "generating";
|
|
5675
|
-
|
|
6375
|
+
logger26.info("Generating SDD", { sessionId });
|
|
5676
6376
|
const prompt = buildGeneratePrompt(session.transcript);
|
|
5677
6377
|
const result = await this.generatorRunner.run({
|
|
5678
6378
|
prompt,
|
|
@@ -5698,7 +6398,7 @@ var BrainstormService = class {
|
|
|
5698
6398
|
const session = this.requireSession(sessionId);
|
|
5699
6399
|
const roundNum = session.rounds.length + 1;
|
|
5700
6400
|
session.status = "reviewing";
|
|
5701
|
-
|
|
6401
|
+
logger26.info("Reviewing SDD", { sessionId, round: roundNum });
|
|
5702
6402
|
onEvent?.({ type: "round:start", data: { round: roundNum, phase: "review" }, round: roundNum });
|
|
5703
6403
|
const prompt = buildReviewPrompt(session.currentSdd, roundNum);
|
|
5704
6404
|
const result = await this.reviewerRunner.run({
|
|
@@ -5731,7 +6431,7 @@ var BrainstormService = class {
|
|
|
5731
6431
|
throw new Error("No review round to refine from");
|
|
5732
6432
|
}
|
|
5733
6433
|
session.status = "refining";
|
|
5734
|
-
|
|
6434
|
+
logger26.info("Refining SDD", { sessionId, round: currentRound.round });
|
|
5735
6435
|
const prompt = buildRefinePrompt(currentRound.questions);
|
|
5736
6436
|
const result = await this.generatorRunner.run({
|
|
5737
6437
|
prompt,
|
|
@@ -5818,4 +6518,4 @@ export {
|
|
|
5818
6518
|
PipelineOrchestrator,
|
|
5819
6519
|
BrainstormService
|
|
5820
6520
|
};
|
|
5821
|
-
//# sourceMappingURL=chunk-
|
|
6521
|
+
//# sourceMappingURL=chunk-WZGEYHCC.js.map
|