@xdevops/issue-auto-finish 1.0.89 → 1.0.91
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-YNRKPQLS.js → chunk-CQ66LL7P.js} +2 -2
- package/dist/{chunk-5JBADEKR.js → chunk-ENF24C44.js} +3 -3
- package/dist/{chunk-IWSMQXBL.js → chunk-LDGK5NMS.js} +775 -261
- package/dist/chunk-LDGK5NMS.js.map +1 -0
- package/dist/{chunk-JMACM7AJ.js → chunk-SEO57UYI.js} +18 -5
- package/dist/chunk-SEO57UYI.js.map +1 -0
- package/dist/{chunk-XSX3PGQW.js → chunk-UMQYEYLO.js} +3 -3
- package/dist/{chunk-DVNAH2GV.js → chunk-Z3OBKODV.js} +11 -10
- package/dist/chunk-Z3OBKODV.js.map +1 -0
- package/dist/cli/setup/ConfigGenerator.d.ts +2 -0
- package/dist/cli/setup/ConfigGenerator.d.ts.map +1 -1
- package/dist/cli/setup/DependencyChecker.d.ts +1 -1
- package/dist/cli/setup/DependencyChecker.d.ts.map +1 -1
- package/dist/cli.js +4 -4
- package/dist/{doctor-ZG3DO7J5.js → doctor-LLETZLW2.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 +5 -5
- package/dist/{init-37DLQ5AJ.js → init-UKTP7LXS.js} +4 -4
- package/dist/lib.js +1 -1
- package/dist/orchestrator/steps/PhaseLoopStep.d.ts.map +1 -1
- package/dist/orchestrator/steps/SetupStep.d.ts.map +1 -1
- package/dist/phases/BasePhase.d.ts +2 -0
- package/dist/phases/BasePhase.d.ts.map +1 -1
- package/dist/{restart-C7QBXT44.js → restart-MSUWF4ID.js} +3 -3
- package/dist/run.js +5 -5
- package/dist/{start-66JO56AW.js → start-B4CDZAFG.js} +3 -3
- package/package.json +1 -1
- package/src/web/frontend/dist/assets/index-BoYtsxGN.js +151 -0
- package/src/web/frontend/dist/assets/index-DWOHf3bd.css +1 -0
- package/src/web/frontend/dist/index.html +2 -2
- package/dist/chunk-DVNAH2GV.js.map +0 -1
- package/dist/chunk-IWSMQXBL.js.map +0 -1
- package/dist/chunk-JMACM7AJ.js.map +0 -1
- package/src/web/frontend/dist/assets/index-DJzC2saL.css +0 -1
- package/src/web/frontend/dist/assets/index-Mnu8M3ww.js +0 -151
- /package/dist/{chunk-YNRKPQLS.js.map → chunk-CQ66LL7P.js.map} +0 -0
- /package/dist/{chunk-5JBADEKR.js.map → chunk-ENF24C44.js.map} +0 -0
- /package/dist/{chunk-XSX3PGQW.js.map → chunk-UMQYEYLO.js.map} +0 -0
- /package/dist/{doctor-ZG3DO7J5.js.map → doctor-LLETZLW2.js.map} +0 -0
- /package/dist/{init-37DLQ5AJ.js.map → init-UKTP7LXS.js.map} +0 -0
- /package/dist/{restart-C7QBXT44.js.map → restart-MSUWF4ID.js.map} +0 -0
- /package/dist/{start-66JO56AW.js.map → start-B4CDZAFG.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);
|
|
@@ -3873,7 +4361,7 @@ function clearPendingDialog(issueIid) {
|
|
|
3873
4361
|
}
|
|
3874
4362
|
|
|
3875
4363
|
// src/orchestrator/steps/PhaseLoopStep.ts
|
|
3876
|
-
var
|
|
4364
|
+
var logger17 = logger.child("PhaseLoopStep");
|
|
3877
4365
|
function resolveVerifyRunner(deps) {
|
|
3878
4366
|
return deps.aiRunner;
|
|
3879
4367
|
}
|
|
@@ -3889,7 +4377,7 @@ async function commitPlanFiles(ctx, wtGit, wtGitMap, phaseName, displayId) {
|
|
|
3889
4377
|
for (const repo of ctx.workspace.repos) {
|
|
3890
4378
|
const repoGit = wtGitMap?.get(repo.name);
|
|
3891
4379
|
if (!repoGit) {
|
|
3892
|
-
|
|
4380
|
+
logger17.warn("Missing GitOperations for repo, skipping commit", { repo: repo.name });
|
|
3893
4381
|
continue;
|
|
3894
4382
|
}
|
|
3895
4383
|
const branch = repo.branchPrefix ? `${repo.branchPrefix}-${displayId}` : ctx.branchName;
|
|
@@ -3898,10 +4386,10 @@ async function commitPlanFiles(ctx, wtGit, wtGitMap, phaseName, displayId) {
|
|
|
3898
4386
|
await repoGit.add(["."]);
|
|
3899
4387
|
await repoGit.commit(commitMsg);
|
|
3900
4388
|
await repoGit.push(branch);
|
|
3901
|
-
|
|
4389
|
+
logger17.info("Committed changes for repo", { repo: repo.name, branch });
|
|
3902
4390
|
}
|
|
3903
4391
|
} catch (err) {
|
|
3904
|
-
|
|
4392
|
+
logger17.warn("Failed to commit/push for repo", {
|
|
3905
4393
|
repo: repo.name,
|
|
3906
4394
|
error: err.message
|
|
3907
4395
|
});
|
|
@@ -3939,10 +4427,10 @@ async function syncResultToIssue(phase, ctx, displayId, phaseName, deps, issueId
|
|
|
3939
4427
|
summary
|
|
3940
4428
|
);
|
|
3941
4429
|
await safeComment(deps, issueId, comment);
|
|
3942
|
-
|
|
4430
|
+
logger17.info("Result synced to issue", { issueIid: displayId, file: file.filename });
|
|
3943
4431
|
}
|
|
3944
4432
|
} catch (err) {
|
|
3945
|
-
|
|
4433
|
+
logger17.warn("Failed to sync result to issue", { error: err.message });
|
|
3946
4434
|
await safeComment(deps, issueId, issueProgressComment(phaseName, "completed"));
|
|
3947
4435
|
}
|
|
3948
4436
|
}
|
|
@@ -3959,7 +4447,7 @@ function buildInputHandler(displayId, phaseName, deps) {
|
|
|
3959
4447
|
};
|
|
3960
4448
|
}
|
|
3961
4449
|
function handlePlanApproval(displayId, phaseName, deps) {
|
|
3962
|
-
|
|
4450
|
+
logger17.info("ACP plan-approval requested, delegating to review gate", {
|
|
3963
4451
|
issueIid: displayId,
|
|
3964
4452
|
phase: phaseName
|
|
3965
4453
|
});
|
|
@@ -3969,14 +4457,14 @@ function handlePlanApproval(displayId, phaseName, deps) {
|
|
|
3969
4457
|
const data = payload.data;
|
|
3970
4458
|
if (data.issueIid !== displayId) return;
|
|
3971
4459
|
cleanup();
|
|
3972
|
-
|
|
4460
|
+
logger17.info("ACP plan-approval approved via review gate", { issueIid: displayId });
|
|
3973
4461
|
resolve("allow");
|
|
3974
4462
|
};
|
|
3975
4463
|
const onRejected = (payload) => {
|
|
3976
4464
|
const data = payload.data;
|
|
3977
4465
|
if (data.issueIid !== displayId) return;
|
|
3978
4466
|
cleanup();
|
|
3979
|
-
|
|
4467
|
+
logger17.info("ACP plan-approval rejected via review gate", { issueIid: displayId });
|
|
3980
4468
|
resolve("reject");
|
|
3981
4469
|
};
|
|
3982
4470
|
const cleanup = () => {
|
|
@@ -3988,7 +4476,7 @@ function handlePlanApproval(displayId, phaseName, deps) {
|
|
|
3988
4476
|
});
|
|
3989
4477
|
}
|
|
3990
4478
|
function handleInteractiveDialog(displayId, phaseName, deps, request) {
|
|
3991
|
-
|
|
4479
|
+
logger17.info("Interactive dialog forwarded to frontend", {
|
|
3992
4480
|
issueIid: displayId,
|
|
3993
4481
|
phase: phaseName,
|
|
3994
4482
|
question: request.content.slice(0, 80),
|
|
@@ -4018,7 +4506,7 @@ function handleInteractiveDialog(displayId, phaseName, deps, request) {
|
|
|
4018
4506
|
if (data.issueIid !== displayId) return;
|
|
4019
4507
|
cleanup();
|
|
4020
4508
|
clearPendingDialog(displayId);
|
|
4021
|
-
|
|
4509
|
+
logger17.info("Interactive dialog response received from frontend", {
|
|
4022
4510
|
issueIid: displayId,
|
|
4023
4511
|
response: data.response
|
|
4024
4512
|
});
|
|
@@ -4028,13 +4516,36 @@ function handleInteractiveDialog(displayId, phaseName, deps, request) {
|
|
|
4028
4516
|
const data = payload.data;
|
|
4029
4517
|
if (data.issueIid !== displayId) return;
|
|
4030
4518
|
cleanup();
|
|
4031
|
-
|
|
4519
|
+
logger17.info("Interactive dialog dismissed by user (false positive)", { issueIid: displayId });
|
|
4032
4520
|
resolve("");
|
|
4033
4521
|
};
|
|
4034
4522
|
deps.eventBus.on("agent:inputResponse", onResponse);
|
|
4035
4523
|
deps.eventBus.on("agent:dialogDismissed", onDismiss);
|
|
4036
4524
|
});
|
|
4037
4525
|
}
|
|
4526
|
+
function updateHooksForPhase(spec, pipelineDef, ctx, wtPlan) {
|
|
4527
|
+
const phaseArtifacts = (spec.artifacts ?? []).map((a) => a.filename);
|
|
4528
|
+
if (phaseArtifacts.length === 0 && spec.kind !== "ai") return;
|
|
4529
|
+
try {
|
|
4530
|
+
const allArtifacts = pipelineDef.phases.flatMap((p) => p.artifacts ?? []).map((a) => a.filename);
|
|
4531
|
+
const hookInjector = new HookInjector();
|
|
4532
|
+
hookInjector.updateForPhase({
|
|
4533
|
+
workDir: wtPlan.baseDir,
|
|
4534
|
+
planDir: wtPlan.planDir,
|
|
4535
|
+
expectedArtifacts: allArtifacts.length > 0 ? allArtifacts : phaseArtifacts,
|
|
4536
|
+
phaseExpectedArtifacts: phaseArtifacts,
|
|
4537
|
+
issueIid: ctx.issue.iid,
|
|
4538
|
+
phaseName: spec.name,
|
|
4539
|
+
issueTitle: ctx.issue.title,
|
|
4540
|
+
issueDescription: ctx.issue.description
|
|
4541
|
+
});
|
|
4542
|
+
} catch (err) {
|
|
4543
|
+
logger17.warn("Failed to update hooks for phase (non-blocking)", {
|
|
4544
|
+
phase: spec.name,
|
|
4545
|
+
error: err.message
|
|
4546
|
+
});
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4038
4549
|
async function safeComment(deps, issueId, message) {
|
|
4039
4550
|
try {
|
|
4040
4551
|
await deps.gongfeng.createIssueNote(issueId, message);
|
|
@@ -4120,15 +4631,15 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
|
4120
4631
|
if (skippedDeployPhase && !phaseCtx.ports) {
|
|
4121
4632
|
const existingPorts = deps.getPortsForIssue(issue.iid);
|
|
4122
4633
|
if (existingPorts && deps.isPreviewRunning(issue.iid)) {
|
|
4123
|
-
|
|
4634
|
+
logger17.info("Restored preview ports from allocator", { iid: issue.iid, ...existingPorts });
|
|
4124
4635
|
phaseCtx.ports = existingPorts;
|
|
4125
4636
|
ctx.wtCtx.ports = existingPorts;
|
|
4126
4637
|
serversStarted = true;
|
|
4127
4638
|
} else {
|
|
4128
4639
|
if (existingPorts) {
|
|
4129
|
-
|
|
4640
|
+
logger17.info("Ports allocated but servers not running, restarting", { iid: issue.iid });
|
|
4130
4641
|
} else {
|
|
4131
|
-
|
|
4642
|
+
logger17.info("Restarting preview servers for resumed pipeline", { iid: issue.iid });
|
|
4132
4643
|
}
|
|
4133
4644
|
const ports = await deps.startPreviewServers(ctx.wtCtx, issue);
|
|
4134
4645
|
if (ports) {
|
|
@@ -4147,7 +4658,7 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
|
4147
4658
|
const prevSpec = pipelineDef.phases[i];
|
|
4148
4659
|
const pp = currentProgress.phases[prevSpec.name];
|
|
4149
4660
|
if (pp && pp.status !== "completed") {
|
|
4150
|
-
|
|
4661
|
+
logger17.warn("Fixing stale phase progress", {
|
|
4151
4662
|
iid: issue.iid,
|
|
4152
4663
|
phase: prevSpec.name,
|
|
4153
4664
|
was: pp.status,
|
|
@@ -4188,7 +4699,7 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
|
4188
4699
|
}
|
|
4189
4700
|
if (spec.kind === "gate") {
|
|
4190
4701
|
if (deps.shouldAutoApprove(issue.labels)) {
|
|
4191
|
-
|
|
4702
|
+
logger17.info("Auto-approving review gate (matched autoApproveLabels)", {
|
|
4192
4703
|
iid: issue.iid,
|
|
4193
4704
|
labels: issue.labels,
|
|
4194
4705
|
autoApproveLabels: deps.config.review.autoApproveLabels
|
|
@@ -4217,7 +4728,7 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
|
4217
4728
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4218
4729
|
});
|
|
4219
4730
|
deps.eventBus.emitTyped("review:requested", { issueIid: issue.iid });
|
|
4220
|
-
|
|
4731
|
+
logger17.info("Review gate reached, pausing", { iid: issue.iid });
|
|
4221
4732
|
return { serversStarted, paused: true };
|
|
4222
4733
|
}
|
|
4223
4734
|
if (spec.name === "verify" && deps.config.verifyFixLoop.enabled) {
|
|
@@ -4226,7 +4737,7 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
|
4226
4737
|
continue;
|
|
4227
4738
|
}
|
|
4228
4739
|
if (spec.name === "uat" && !isE2eEnabledForIssue(issue.iid, deps.tracker, deps.config)) {
|
|
4229
|
-
|
|
4740
|
+
logger17.info("UAT phase skipped (E2E not enabled for this issue)", { iid: issue.iid });
|
|
4230
4741
|
deps.tracker.updateState(issue.iid, spec.doneState, { currentPhase: spec.name });
|
|
4231
4742
|
wtPlan.updatePhaseProgress(spec.name, "completed");
|
|
4232
4743
|
deps.tracker.updatePhaseProgress(issue.iid, spec.name, {
|
|
@@ -4235,10 +4746,11 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
|
4235
4746
|
});
|
|
4236
4747
|
continue;
|
|
4237
4748
|
}
|
|
4749
|
+
updateHooksForPhase(spec, pipelineDef, ctx, wtPlan);
|
|
4238
4750
|
const runner = spec.name === "verify" ? resolveVerifyRunner(deps) : spec.name === "uat" ? resolveUatRunner(deps, issue.iid) : deps.aiRunner;
|
|
4239
4751
|
if (spec.name === "uat") {
|
|
4240
4752
|
const runnerName = runner === deps.e2eAiRunner ? "e2eAiRunner (CodeBuddy)" : "mainRunner";
|
|
4241
|
-
|
|
4753
|
+
logger17.info("UAT phase starting", { iid: issue.iid, runner: runnerName });
|
|
4242
4754
|
}
|
|
4243
4755
|
const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config);
|
|
4244
4756
|
if (wtGitMap) {
|
|
@@ -4256,7 +4768,7 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
|
4256
4768
|
);
|
|
4257
4769
|
if (outcome.status === "running") {
|
|
4258
4770
|
if (outcome.awaitCompletion) {
|
|
4259
|
-
|
|
4771
|
+
logger17.info("Async phase running, awaiting completion", { iid: issue.iid, phase: spec.name });
|
|
4260
4772
|
const finalOutcome = await awaitAsyncPhase(outcome, spec, ctx, deps, wtGit, wtPlan, wtGitMap);
|
|
4261
4773
|
if (finalOutcome.status === "completed") {
|
|
4262
4774
|
continue;
|
|
@@ -4267,7 +4779,7 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
|
4267
4779
|
deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "gate_waiting" });
|
|
4268
4780
|
const gateEvent = spec.name === "uat" ? "uat:gateRequested" : "release:gateRequested";
|
|
4269
4781
|
deps.eventBus.emitTyped(gateEvent, { issueIid: issue.iid });
|
|
4270
|
-
|
|
4782
|
+
logger17.info("Async phase running (no awaitCompletion), pausing pipeline", { iid: issue.iid, phase: spec.name });
|
|
4271
4783
|
return { serversStarted, paused: true };
|
|
4272
4784
|
}
|
|
4273
4785
|
if (spec.approvedState && outcome.data?.hasReleaseCapability) {
|
|
@@ -4275,7 +4787,7 @@ async function executePhaseLoop(ctx, deps, wtGit, wtPlan, wtGitMap) {
|
|
|
4275
4787
|
wtPlan.updatePhaseProgress(spec.name, "gate_waiting");
|
|
4276
4788
|
deps.tracker.updatePhaseProgress(issue.iid, spec.name, { status: "gate_waiting" });
|
|
4277
4789
|
deps.eventBus.emitTyped("release:gateRequested", { issueIid: issue.iid });
|
|
4278
|
-
|
|
4790
|
+
logger17.info("Phase requested gate, pausing", { iid: issue.iid, phase: spec.name });
|
|
4279
4791
|
return { serversStarted, paused: true };
|
|
4280
4792
|
}
|
|
4281
4793
|
if (needsDeployment && !serversStarted && lifecycleManager.shouldDeployPreview(spec.name)) {
|
|
@@ -4308,7 +4820,7 @@ async function awaitAsyncPhase(outcome, spec, ctx, deps, wtGit, wtPlan, wtGitMap
|
|
|
4308
4820
|
const runner = spec.name === "uat" ? resolveUatRunner(deps, displayId) : deps.aiRunner;
|
|
4309
4821
|
const phase = createPhase(spec.name, runner, wtGit, wtPlan, deps.config);
|
|
4310
4822
|
await syncResultToIssue(phase, phaseCtx, displayId, spec.name, deps, issue.id, wtPlan);
|
|
4311
|
-
|
|
4823
|
+
logger17.info("Async phase completed successfully", { iid: displayId, phase: spec.name });
|
|
4312
4824
|
return finalOutcome;
|
|
4313
4825
|
}
|
|
4314
4826
|
const errMsg = finalOutcome.error?.message ?? "Unknown error";
|
|
@@ -4343,7 +4855,7 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
|
|
|
4343
4855
|
issueIid: issue.iid,
|
|
4344
4856
|
maxIterations
|
|
4345
4857
|
});
|
|
4346
|
-
|
|
4858
|
+
logger17.info("Verify-fix loop started", {
|
|
4347
4859
|
iid: issue.iid,
|
|
4348
4860
|
maxIterations,
|
|
4349
4861
|
buildPhaseIdx
|
|
@@ -4352,11 +4864,12 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
|
|
|
4352
4864
|
if (isShuttingDown()) {
|
|
4353
4865
|
throw new ServiceShutdownError();
|
|
4354
4866
|
}
|
|
4355
|
-
|
|
4867
|
+
logger17.info("Verify-fix loop iteration", {
|
|
4356
4868
|
iteration,
|
|
4357
4869
|
maxIterations,
|
|
4358
4870
|
iid: issue.iid
|
|
4359
4871
|
});
|
|
4872
|
+
updateHooksForPhase(verifySpec, ctx.pipelineDef, ctx, wtPlan);
|
|
4360
4873
|
const verifyRunner = resolveVerifyRunner(deps);
|
|
4361
4874
|
const verifyPhase = createPhase("verify", verifyRunner, wtGit, wtPlan, deps.config);
|
|
4362
4875
|
if (wtGitMap) {
|
|
@@ -4375,7 +4888,7 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
|
|
|
4375
4888
|
wtGitMap
|
|
4376
4889
|
);
|
|
4377
4890
|
} catch (err) {
|
|
4378
|
-
|
|
4891
|
+
logger17.warn("Verify phase execution failed", {
|
|
4379
4892
|
iteration,
|
|
4380
4893
|
iid: issue.iid,
|
|
4381
4894
|
error: err.message
|
|
@@ -4407,13 +4920,13 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
|
|
|
4407
4920
|
failures: report?.failureReasons
|
|
4408
4921
|
});
|
|
4409
4922
|
if (passed) {
|
|
4410
|
-
|
|
4923
|
+
logger17.info("Verify-fix loop passed", {
|
|
4411
4924
|
iteration,
|
|
4412
4925
|
iid: issue.iid
|
|
4413
4926
|
});
|
|
4414
4927
|
return;
|
|
4415
4928
|
}
|
|
4416
|
-
|
|
4929
|
+
logger17.info("Verify failed, issues found", {
|
|
4417
4930
|
iteration,
|
|
4418
4931
|
iid: issue.iid,
|
|
4419
4932
|
failures: report?.failureReasons,
|
|
@@ -4426,7 +4939,7 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
|
|
|
4426
4939
|
failures: report?.failureReasons ?? []
|
|
4427
4940
|
});
|
|
4428
4941
|
const failMsg = `Verify-fix loop exhausted after ${maxIterations} iterations. Remaining issues: ${report?.failureReasons?.join("; ") ?? "unknown"}`;
|
|
4429
|
-
|
|
4942
|
+
logger17.warn(failMsg, { iid: issue.iid });
|
|
4430
4943
|
throw new AIExecutionError("verify", failMsg, {
|
|
4431
4944
|
output: report?.rawReport ?? "",
|
|
4432
4945
|
exitCode: 0
|
|
@@ -4444,13 +4957,14 @@ async function executeVerifyFixLoop(ctx, deps, wtGit, wtPlan, verifyPhaseIdx, bu
|
|
|
4444
4957
|
async function executeBuildFix(ctx, deps, wtGit, wtPlan, buildPhaseIdx, fixContext, wtGitMap) {
|
|
4445
4958
|
const { issue, phaseCtx } = ctx;
|
|
4446
4959
|
const buildSpec = ctx.pipelineDef.phases[buildPhaseIdx];
|
|
4447
|
-
|
|
4960
|
+
logger17.info("Looping back to build for fix", {
|
|
4448
4961
|
iteration: fixContext.iteration,
|
|
4449
4962
|
iid: issue.iid,
|
|
4450
4963
|
failures: fixContext.verifyFailures
|
|
4451
4964
|
});
|
|
4452
4965
|
phaseCtx.fixContext = fixContext;
|
|
4453
4966
|
try {
|
|
4967
|
+
updateHooksForPhase(buildSpec, ctx.pipelineDef, ctx, wtPlan);
|
|
4454
4968
|
const buildPhase = createPhase("build", deps.aiRunner, wtGit, wtPlan, deps.config);
|
|
4455
4969
|
if (wtGitMap) {
|
|
4456
4970
|
buildPhase.setWtGitMap(wtGitMap);
|
|
@@ -4471,7 +4985,7 @@ async function executeBuildFix(ctx, deps, wtGit, wtPlan, buildPhaseIdx, fixConte
|
|
|
4471
4985
|
}
|
|
4472
4986
|
|
|
4473
4987
|
// src/orchestrator/steps/CompletionStep.ts
|
|
4474
|
-
var
|
|
4988
|
+
var logger18 = logger.child("CompletionStep");
|
|
4475
4989
|
async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
|
|
4476
4990
|
const { issue, branchName, wtCtx } = ctx;
|
|
4477
4991
|
deps.emitProgress(issue.iid, "create_mr", t("orchestrator.createMrProgress"));
|
|
@@ -4503,7 +5017,7 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
|
|
|
4503
5017
|
mrIid: void 0
|
|
4504
5018
|
});
|
|
4505
5019
|
} catch (err) {
|
|
4506
|
-
|
|
5020
|
+
logger18.warn("Failed to publish E2E screenshots", {
|
|
4507
5021
|
iid: issue.iid,
|
|
4508
5022
|
error: err.message
|
|
4509
5023
|
});
|
|
@@ -4523,19 +5037,19 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
|
|
|
4523
5037
|
await deps.claimer.releaseClaim(issue.id, issue.iid, "completed");
|
|
4524
5038
|
}
|
|
4525
5039
|
if (phaseResult.serversStarted && deps.config.preview.keepAfterComplete) {
|
|
4526
|
-
|
|
5040
|
+
logger18.info("Preview servers kept running after completion", { iid: issue.iid });
|
|
4527
5041
|
} else {
|
|
4528
5042
|
deps.stopPreviewServers(issue.iid);
|
|
4529
5043
|
await deps.mainGitMutex.runExclusive(async () => {
|
|
4530
5044
|
if (wtCtx.workspace) {
|
|
4531
5045
|
await deps.workspaceManager.cleanupWorkspace(wtCtx.workspace);
|
|
4532
|
-
|
|
5046
|
+
logger18.info("Workspace cleaned up", { dir: wtCtx.workspace.workspaceRoot });
|
|
4533
5047
|
} else {
|
|
4534
5048
|
try {
|
|
4535
5049
|
await deps.mainGit.worktreeRemove(wtCtx.gitRootDir, true);
|
|
4536
|
-
|
|
5050
|
+
logger18.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
|
|
4537
5051
|
} catch (err) {
|
|
4538
|
-
|
|
5052
|
+
logger18.warn("Failed to cleanup worktree", {
|
|
4539
5053
|
dir: wtCtx.gitRootDir,
|
|
4540
5054
|
error: err.message
|
|
4541
5055
|
});
|
|
@@ -4543,16 +5057,16 @@ async function executeCompletion(ctx, deps, phaseResult, _wtGitMap) {
|
|
|
4543
5057
|
}
|
|
4544
5058
|
});
|
|
4545
5059
|
}
|
|
4546
|
-
|
|
5060
|
+
logger18.info("Issue processing completed", { iid: issue.iid });
|
|
4547
5061
|
}
|
|
4548
5062
|
|
|
4549
5063
|
// src/orchestrator/steps/FailureHandler.ts
|
|
4550
|
-
var
|
|
5064
|
+
var logger19 = logger.child("FailureHandler");
|
|
4551
5065
|
async function handleFailure(err, issue, wtCtx, deps) {
|
|
4552
5066
|
const errorMsg = err.message;
|
|
4553
5067
|
const isRetryable = err instanceof AIExecutionError ? err.isRetryable : true;
|
|
4554
5068
|
const wasActiveAtTimeout = err instanceof AIExecutionError && err.wasActiveAtTimeout;
|
|
4555
|
-
|
|
5069
|
+
logger19.error("Issue processing failed", { iid: issue.iid, error: errorMsg, isRetryable, wasActiveAtTimeout });
|
|
4556
5070
|
metrics.incCounter("iaf_issues_failed_total");
|
|
4557
5071
|
const currentRecord = deps.tracker.get(issue.iid);
|
|
4558
5072
|
const failedAtState = currentRecord?.state || "pending" /* Pending */;
|
|
@@ -4565,11 +5079,11 @@ async function handleFailure(err, issue, wtCtx, deps) {
|
|
|
4565
5079
|
}
|
|
4566
5080
|
}
|
|
4567
5081
|
if (wasReset) {
|
|
4568
|
-
|
|
5082
|
+
logger19.info("Issue was reset during processing, skipping failure marking", { iid: issue.iid });
|
|
4569
5083
|
throw err;
|
|
4570
5084
|
}
|
|
4571
5085
|
if (failedAtState === "paused" /* Paused */) {
|
|
4572
|
-
|
|
5086
|
+
logger19.info("Issue was paused during processing, skipping failure handling", { iid: issue.iid });
|
|
4573
5087
|
throw err;
|
|
4574
5088
|
}
|
|
4575
5089
|
try {
|
|
@@ -4591,7 +5105,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
|
|
|
4591
5105
|
try {
|
|
4592
5106
|
await deps.claimer.releaseClaim(issue.id, issue.iid, "failed");
|
|
4593
5107
|
} catch (releaseErr) {
|
|
4594
|
-
|
|
5108
|
+
logger19.warn("Failed to release lock on failure", {
|
|
4595
5109
|
iid: issue.iid,
|
|
4596
5110
|
error: releaseErr.message
|
|
4597
5111
|
});
|
|
@@ -4599,7 +5113,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
|
|
|
4599
5113
|
}
|
|
4600
5114
|
deps.stopPreviewServers(issue.iid);
|
|
4601
5115
|
const preservedDirs = wtCtx.workspace ? [wtCtx.workspace.primary.gitRootDir, ...wtCtx.workspace.associates.map((a) => a.gitRootDir)] : [wtCtx.gitRootDir];
|
|
4602
|
-
|
|
5116
|
+
logger19.info("Worktree(s) preserved for debugging", {
|
|
4603
5117
|
primary: wtCtx.gitRootDir,
|
|
4604
5118
|
all: preservedDirs
|
|
4605
5119
|
});
|
|
@@ -4608,7 +5122,7 @@ async function handleFailure(err, issue, wtCtx, deps) {
|
|
|
4608
5122
|
|
|
4609
5123
|
// src/orchestrator/PipelineOrchestrator.ts
|
|
4610
5124
|
var execFileAsync2 = promisify2(execFile2);
|
|
4611
|
-
var
|
|
5125
|
+
var logger20 = logger.child("PipelineOrchestrator");
|
|
4612
5126
|
var PipelineOrchestrator = class {
|
|
4613
5127
|
config;
|
|
4614
5128
|
gongfeng;
|
|
@@ -4638,7 +5152,7 @@ var PipelineOrchestrator = class {
|
|
|
4638
5152
|
setAIRunner(runner) {
|
|
4639
5153
|
this.aiRunner = runner;
|
|
4640
5154
|
this.conflictResolver = new ConflictResolver(runner);
|
|
4641
|
-
|
|
5155
|
+
logger20.info("AIRunner replaced via hot-reload");
|
|
4642
5156
|
}
|
|
4643
5157
|
constructor(config, gongfeng, git, aiRunner, tracker, supplementStore, mainGitMutex, eventBusInstance, wsConfig, tenantId, e2eAiRunner) {
|
|
4644
5158
|
this.config = config;
|
|
@@ -4656,14 +5170,14 @@ var PipelineOrchestrator = class {
|
|
|
4656
5170
|
this.pipelineDef = mode === "plan-mode" ? buildPlanModePipeline({ releaseEnabled: config.release.enabled, e2eEnabled: config.e2e.enabled }) : getPipelineDef(mode);
|
|
4657
5171
|
registerPipeline(this.pipelineDef);
|
|
4658
5172
|
this.lifecycleManager = createLifecycleManager(this.pipelineDef);
|
|
4659
|
-
|
|
5173
|
+
logger20.info("Pipeline mode resolved", { tenantId: this.tenantId, mode: this.pipelineDef.mode, aiMode: config.ai.mode });
|
|
4660
5174
|
this.portAllocator = new PortAllocator({
|
|
4661
5175
|
backendPortBase: config.e2e.backendPortBase,
|
|
4662
5176
|
frontendPortBase: config.e2e.frontendPortBase
|
|
4663
5177
|
});
|
|
4664
5178
|
this.devServerManager = new DevServerManager();
|
|
4665
5179
|
this.screenshotPublisher = new ScreenshotPublisher(gongfeng);
|
|
4666
|
-
this.effectiveWorktreeBaseDir = this.tenantId === "default" ? config.project.worktreeBaseDir :
|
|
5180
|
+
this.effectiveWorktreeBaseDir = this.tenantId === "default" ? config.project.worktreeBaseDir : path12.join(config.project.worktreeBaseDir, this.tenantId);
|
|
4667
5181
|
const effectiveWsConfig = wsConfig ?? buildSingleRepoWorkspace(config.project, config.gongfeng.projectPath);
|
|
4668
5182
|
this.workspaceManager = new WorkspaceManager({
|
|
4669
5183
|
wsConfig: effectiveWsConfig,
|
|
@@ -4672,7 +5186,7 @@ var PipelineOrchestrator = class {
|
|
|
4672
5186
|
mainGitMutex: this.mainGitMutex,
|
|
4673
5187
|
gongfengApiUrl: config.gongfeng.apiUrl
|
|
4674
5188
|
});
|
|
4675
|
-
|
|
5189
|
+
logger20.info("WorkspaceManager initialized", {
|
|
4676
5190
|
tenantId: this.tenantId,
|
|
4677
5191
|
primary: effectiveWsConfig.primary.name,
|
|
4678
5192
|
associates: effectiveWsConfig.associates.map((a) => a.name)
|
|
@@ -4693,7 +5207,7 @@ var PipelineOrchestrator = class {
|
|
|
4693
5207
|
this.claimer = claimer;
|
|
4694
5208
|
}
|
|
4695
5209
|
async cleanupStaleState() {
|
|
4696
|
-
|
|
5210
|
+
logger20.info("Cleaning up stale worktree state...");
|
|
4697
5211
|
let cleaned = 0;
|
|
4698
5212
|
const repoGitRoot = this.config.project.gitRootDir;
|
|
4699
5213
|
try {
|
|
@@ -4702,11 +5216,11 @@ var PipelineOrchestrator = class {
|
|
|
4702
5216
|
if (wtDir === repoGitRoot) continue;
|
|
4703
5217
|
if (!wtDir.includes("/issue-")) continue;
|
|
4704
5218
|
try {
|
|
4705
|
-
const gitFile =
|
|
5219
|
+
const gitFile = path12.join(wtDir, ".git");
|
|
4706
5220
|
try {
|
|
4707
|
-
await
|
|
5221
|
+
await fs11.access(gitFile);
|
|
4708
5222
|
} catch {
|
|
4709
|
-
|
|
5223
|
+
logger20.warn("Worktree corrupted (.git missing), force removing", { dir: wtDir });
|
|
4710
5224
|
await this.mainGit.worktreeRemove(wtDir, true).catch(() => {
|
|
4711
5225
|
});
|
|
4712
5226
|
await this.mainGit.worktreePrune();
|
|
@@ -4715,32 +5229,32 @@ var PipelineOrchestrator = class {
|
|
|
4715
5229
|
}
|
|
4716
5230
|
const wtGit = new GitOperations(wtDir);
|
|
4717
5231
|
if (await wtGit.isRebaseInProgress()) {
|
|
4718
|
-
|
|
5232
|
+
logger20.warn("Aborting residual rebase in worktree", { dir: wtDir });
|
|
4719
5233
|
await wtGit.rebaseAbort();
|
|
4720
5234
|
cleaned++;
|
|
4721
5235
|
}
|
|
4722
|
-
const indexLock =
|
|
5236
|
+
const indexLock = path12.join(wtDir, ".git", "index.lock");
|
|
4723
5237
|
try {
|
|
4724
|
-
await
|
|
4725
|
-
|
|
5238
|
+
await fs11.unlink(indexLock);
|
|
5239
|
+
logger20.warn("Removed stale index.lock", { path: indexLock });
|
|
4726
5240
|
cleaned++;
|
|
4727
5241
|
} catch {
|
|
4728
5242
|
}
|
|
4729
5243
|
} catch (err) {
|
|
4730
|
-
|
|
5244
|
+
logger20.warn("Failed to clean worktree state", { dir: wtDir, error: err.message });
|
|
4731
5245
|
}
|
|
4732
5246
|
}
|
|
4733
5247
|
} catch (err) {
|
|
4734
|
-
|
|
5248
|
+
logger20.warn("Failed to list worktrees for cleanup", { error: err.message });
|
|
4735
5249
|
}
|
|
4736
|
-
const mainIndexLock =
|
|
5250
|
+
const mainIndexLock = path12.join(repoGitRoot, ".git", "index.lock");
|
|
4737
5251
|
try {
|
|
4738
|
-
await
|
|
4739
|
-
|
|
5252
|
+
await fs11.unlink(mainIndexLock);
|
|
5253
|
+
logger20.warn("Removed stale main repo index.lock", { path: mainIndexLock });
|
|
4740
5254
|
cleaned++;
|
|
4741
5255
|
} catch {
|
|
4742
5256
|
}
|
|
4743
|
-
|
|
5257
|
+
logger20.info("Stale state cleanup complete", { cleaned });
|
|
4744
5258
|
}
|
|
4745
5259
|
/**
|
|
4746
5260
|
* 重启后清理幽灵端口分配。
|
|
@@ -4753,7 +5267,7 @@ var PipelineOrchestrator = class {
|
|
|
4753
5267
|
for (const record of this.tracker.getAll()) {
|
|
4754
5268
|
if (record.ports) {
|
|
4755
5269
|
const iid = getIid(record);
|
|
4756
|
-
|
|
5270
|
+
logger20.info("Clearing stale port allocation after restart", { iid, ports: record.ports });
|
|
4757
5271
|
this.tracker.updateState(iid, record.state, {
|
|
4758
5272
|
ports: void 0,
|
|
4759
5273
|
previewStartedAt: void 0
|
|
@@ -4798,20 +5312,20 @@ var PipelineOrchestrator = class {
|
|
|
4798
5312
|
}
|
|
4799
5313
|
try {
|
|
4800
5314
|
await this.mainGit.worktreeRemove(wtCtx.gitRootDir, true);
|
|
4801
|
-
|
|
5315
|
+
logger20.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
|
|
4802
5316
|
} catch (err) {
|
|
4803
|
-
|
|
5317
|
+
logger20.warn("Failed to cleanup worktree", { dir: wtCtx.gitRootDir, error: err.message });
|
|
4804
5318
|
}
|
|
4805
5319
|
}
|
|
4806
5320
|
async installDependencies(workDir) {
|
|
4807
|
-
|
|
5321
|
+
logger20.info("Installing dependencies in worktree", { workDir });
|
|
4808
5322
|
const knowledge = getProjectKnowledge() ?? KNOWLEDGE_DEFAULTS;
|
|
4809
5323
|
const pkgMgr = knowledge.toolchain.packageManager.toLowerCase();
|
|
4810
5324
|
const isNodeProject = ["npm", "pnpm", "yarn", "bun"].some((m) => pkgMgr.includes(m));
|
|
4811
5325
|
if (isNodeProject) {
|
|
4812
5326
|
const ready = await this.ensureNodeModules(workDir);
|
|
4813
5327
|
if (ready) {
|
|
4814
|
-
|
|
5328
|
+
logger20.info("node_modules ready \u2014 skipping install");
|
|
4815
5329
|
return;
|
|
4816
5330
|
}
|
|
4817
5331
|
}
|
|
@@ -4824,10 +5338,10 @@ var PipelineOrchestrator = class {
|
|
|
4824
5338
|
maxBuffer: 10 * 1024 * 1024,
|
|
4825
5339
|
timeout: 3e5
|
|
4826
5340
|
});
|
|
4827
|
-
|
|
5341
|
+
logger20.info("Dependencies installed");
|
|
4828
5342
|
} catch (err) {
|
|
4829
5343
|
if (fallbackCmd) {
|
|
4830
|
-
|
|
5344
|
+
logger20.warn(`${installCmd} failed, retrying with fallback command`, {
|
|
4831
5345
|
error: err.message
|
|
4832
5346
|
});
|
|
4833
5347
|
const [fallbackBin, ...fallbackArgs] = fallbackCmd.split(/\s+/);
|
|
@@ -4837,45 +5351,45 @@ var PipelineOrchestrator = class {
|
|
|
4837
5351
|
maxBuffer: 10 * 1024 * 1024,
|
|
4838
5352
|
timeout: 3e5
|
|
4839
5353
|
});
|
|
4840
|
-
|
|
5354
|
+
logger20.info("Dependencies installed (fallback)");
|
|
4841
5355
|
} catch (retryErr) {
|
|
4842
|
-
|
|
5356
|
+
logger20.warn("Fallback install also failed", {
|
|
4843
5357
|
error: retryErr.message
|
|
4844
5358
|
});
|
|
4845
5359
|
}
|
|
4846
5360
|
} else {
|
|
4847
|
-
|
|
5361
|
+
logger20.warn("Install failed, no fallback configured", {
|
|
4848
5362
|
error: err.message
|
|
4849
5363
|
});
|
|
4850
5364
|
}
|
|
4851
5365
|
}
|
|
4852
5366
|
}
|
|
4853
5367
|
async ensureNodeModules(workDir) {
|
|
4854
|
-
const targetBin =
|
|
5368
|
+
const targetBin = path12.join(workDir, "node_modules", ".bin");
|
|
4855
5369
|
try {
|
|
4856
|
-
await
|
|
4857
|
-
|
|
5370
|
+
await fs11.access(targetBin);
|
|
5371
|
+
logger20.info("node_modules already complete (has .bin/)");
|
|
4858
5372
|
return true;
|
|
4859
5373
|
} catch {
|
|
4860
5374
|
}
|
|
4861
|
-
const sourceNM =
|
|
4862
|
-
const targetNM =
|
|
5375
|
+
const sourceNM = path12.join(this.config.project.workDir, "node_modules");
|
|
5376
|
+
const targetNM = path12.join(workDir, "node_modules");
|
|
4863
5377
|
try {
|
|
4864
|
-
await
|
|
5378
|
+
await fs11.access(sourceNM);
|
|
4865
5379
|
} catch {
|
|
4866
|
-
|
|
5380
|
+
logger20.warn("Main repo node_modules not found, skipping seed", { sourceNM });
|
|
4867
5381
|
return false;
|
|
4868
5382
|
}
|
|
4869
|
-
|
|
5383
|
+
logger20.info("Seeding node_modules from main repo via reflink copy", { sourceNM, targetNM });
|
|
4870
5384
|
try {
|
|
4871
5385
|
await execFileAsync2("rm", ["-rf", targetNM], { timeout: 6e4 });
|
|
4872
5386
|
await execFileAsync2("cp", ["-a", "--reflink=auto", sourceNM, targetNM], {
|
|
4873
5387
|
timeout: 12e4
|
|
4874
5388
|
});
|
|
4875
|
-
|
|
5389
|
+
logger20.info("node_modules seeded from main repo");
|
|
4876
5390
|
return true;
|
|
4877
5391
|
} catch (err) {
|
|
4878
|
-
|
|
5392
|
+
logger20.warn("Failed to seed node_modules from main repo", {
|
|
4879
5393
|
error: err.message
|
|
4880
5394
|
});
|
|
4881
5395
|
return false;
|
|
@@ -4885,16 +5399,16 @@ var PipelineOrchestrator = class {
|
|
|
4885
5399
|
const record = this.tracker.get(issueIid);
|
|
4886
5400
|
if (!record) throw new IssueNotFoundError(issueIid);
|
|
4887
5401
|
const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
|
|
4888
|
-
|
|
5402
|
+
logger20.info("Restarting issue \u2014 cleaning context", { issueIid, branchName: record.branchName });
|
|
4889
5403
|
this.pendingActions.set(issueIid, "restart");
|
|
4890
5404
|
this.aiRunner.killByWorkDir(wtCtx.workDir);
|
|
4891
5405
|
this.e2eAiRunner?.killByWorkDir(wtCtx.workDir);
|
|
4892
5406
|
this.stopPreviewServers(issueIid);
|
|
4893
5407
|
try {
|
|
4894
5408
|
const deleted = await this.gongfeng.cleanupAgentNotes(getExternalId(record));
|
|
4895
|
-
|
|
5409
|
+
logger20.info("Agent notes cleaned up", { issueIid, deleted });
|
|
4896
5410
|
} catch (err) {
|
|
4897
|
-
|
|
5411
|
+
logger20.warn("Failed to cleanup agent notes", { issueIid, error: err.message });
|
|
4898
5412
|
}
|
|
4899
5413
|
await this.mainGitMutex.runExclusive(async () => {
|
|
4900
5414
|
await this.cleanupWorktree(wtCtx);
|
|
@@ -4911,19 +5425,19 @@ var PipelineOrchestrator = class {
|
|
|
4911
5425
|
await this.cleanupE2eOutputs(issueIid);
|
|
4912
5426
|
this.tracker.resetFull(issueIid);
|
|
4913
5427
|
this.pendingActions.delete(issueIid);
|
|
4914
|
-
|
|
5428
|
+
logger20.info("Issue restarted", { issueIid });
|
|
4915
5429
|
}
|
|
4916
5430
|
async cancelIssue(issueIid) {
|
|
4917
5431
|
const record = this.tracker.get(issueIid);
|
|
4918
5432
|
if (!record) throw new IssueNotFoundError(issueIid);
|
|
4919
5433
|
const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
|
|
4920
|
-
|
|
5434
|
+
logger20.info("Cancelling issue \u2014 cleaning all resources", { issueIid, branchName: record.branchName });
|
|
4921
5435
|
this.aiRunner.killByWorkDir(wtCtx.workDir);
|
|
4922
5436
|
this.stopPreviewServers(issueIid);
|
|
4923
5437
|
try {
|
|
4924
5438
|
await this.gongfeng.removeLabelsWithPrefix(getExternalId(record), "auto-finish");
|
|
4925
5439
|
} catch (err) {
|
|
4926
|
-
|
|
5440
|
+
logger20.warn("Failed to remove labels on cancel", { issueIid, error: err.message });
|
|
4927
5441
|
}
|
|
4928
5442
|
await this.mainGitMutex.runExclusive(async () => {
|
|
4929
5443
|
await this.cleanupWorktree(wtCtx);
|
|
@@ -4940,7 +5454,7 @@ var PipelineOrchestrator = class {
|
|
|
4940
5454
|
this.tracker.clearProcessingLock(issueIid);
|
|
4941
5455
|
this.tracker.updateState(issueIid, "skipped" /* Skipped */);
|
|
4942
5456
|
await this.cleanupE2eOutputs(issueIid);
|
|
4943
|
-
|
|
5457
|
+
logger20.info("Issue cancelled", { issueIid });
|
|
4944
5458
|
}
|
|
4945
5459
|
/**
|
|
4946
5460
|
* Remove the E2E output directory for an issue: {uatVendorDir}/outputs/issue-{iid}
|
|
@@ -4948,13 +5462,13 @@ var PipelineOrchestrator = class {
|
|
|
4948
5462
|
async cleanupE2eOutputs(issueIid) {
|
|
4949
5463
|
const vendorDir = this.config.e2e.uatVendorDir;
|
|
4950
5464
|
if (!vendorDir) return;
|
|
4951
|
-
const abs =
|
|
4952
|
-
const outputDir =
|
|
5465
|
+
const abs = path12.isAbsolute(vendorDir) ? vendorDir : path12.resolve(this.config.project.workDir, vendorDir);
|
|
5466
|
+
const outputDir = path12.join(abs, "outputs", `issue-${issueIid}`);
|
|
4953
5467
|
try {
|
|
4954
|
-
await
|
|
4955
|
-
|
|
5468
|
+
await fs11.rm(outputDir, { recursive: true, force: true });
|
|
5469
|
+
logger20.info("E2E outputs cleaned up", { issueIid, dir: outputDir });
|
|
4956
5470
|
} catch (err) {
|
|
4957
|
-
|
|
5471
|
+
logger20.warn("Failed to cleanup E2E outputs", { issueIid, dir: outputDir, error: err.message });
|
|
4958
5472
|
}
|
|
4959
5473
|
}
|
|
4960
5474
|
/**
|
|
@@ -4966,10 +5480,10 @@ var PipelineOrchestrator = class {
|
|
|
4966
5480
|
if (!this.workspaceManager) return;
|
|
4967
5481
|
const wsRoot = this.workspaceManager.getWorkspaceRoot(issueIid);
|
|
4968
5482
|
try {
|
|
4969
|
-
await
|
|
4970
|
-
|
|
5483
|
+
await fs11.rm(wsRoot, { recursive: true, force: true });
|
|
5484
|
+
logger20.info("Workspace root cleaned up", { issueIid, dir: wsRoot });
|
|
4971
5485
|
} catch (err) {
|
|
4972
|
-
|
|
5486
|
+
logger20.warn("Failed to cleanup workspace root", { issueIid, dir: wsRoot, error: err.message });
|
|
4973
5487
|
}
|
|
4974
5488
|
}
|
|
4975
5489
|
retryFromPhase(issueIid, phase) {
|
|
@@ -4985,7 +5499,7 @@ var PipelineOrchestrator = class {
|
|
|
4985
5499
|
this.aiRunner.killByWorkDir(wtCtx.workDir);
|
|
4986
5500
|
}
|
|
4987
5501
|
this.e2eAiRunner?.killByWorkDir(wtCtx.workDir);
|
|
4988
|
-
|
|
5502
|
+
logger20.info("Retrying issue from phase", { issueIid, phase });
|
|
4989
5503
|
const ok = this.tracker.resetToPhase(issueIid, phase, issueDef);
|
|
4990
5504
|
if (!ok) {
|
|
4991
5505
|
throw new InvalidPhaseError(phase);
|
|
@@ -5012,7 +5526,7 @@ var PipelineOrchestrator = class {
|
|
|
5012
5526
|
} else {
|
|
5013
5527
|
this.tracker.pauseIssue(issueIid, record.currentPhase ?? "");
|
|
5014
5528
|
}
|
|
5015
|
-
|
|
5529
|
+
logger20.info("Issue abort requested", { issueIid, state: record.state });
|
|
5016
5530
|
}
|
|
5017
5531
|
continueIssue(issueIid) {
|
|
5018
5532
|
const record = this.tracker.get(issueIid);
|
|
@@ -5022,7 +5536,7 @@ var PipelineOrchestrator = class {
|
|
|
5022
5536
|
}
|
|
5023
5537
|
const issueDef = this.getIssueSpecificPipelineDef(record);
|
|
5024
5538
|
this.tracker.resumeFromPause(issueIid, issueDef, false);
|
|
5025
|
-
|
|
5539
|
+
logger20.info("Issue continued from pause", { issueIid });
|
|
5026
5540
|
}
|
|
5027
5541
|
redoPhase(issueIid) {
|
|
5028
5542
|
const record = this.tracker.get(issueIid);
|
|
@@ -5066,7 +5580,7 @@ var PipelineOrchestrator = class {
|
|
|
5066
5580
|
}
|
|
5067
5581
|
this.eventBus.emitTyped("issue:redone", { issueIid });
|
|
5068
5582
|
}
|
|
5069
|
-
|
|
5583
|
+
logger20.info("Issue redo requested", { issueIid, state: record.state });
|
|
5070
5584
|
}
|
|
5071
5585
|
/**
|
|
5072
5586
|
* 处理中止/重做的共享逻辑:
|
|
@@ -5139,7 +5653,7 @@ var PipelineOrchestrator = class {
|
|
|
5139
5653
|
async _processIssueImpl(issue) {
|
|
5140
5654
|
const branchName = `${this.config.project.branchPrefix}-${issue.iid}`;
|
|
5141
5655
|
const wtCtx = this.computeWorktreeContext(issue.iid, branchName);
|
|
5142
|
-
|
|
5656
|
+
logger20.info("Processing issue", {
|
|
5143
5657
|
iid: issue.iid,
|
|
5144
5658
|
title: issue.title,
|
|
5145
5659
|
branchName,
|
|
@@ -5246,7 +5760,7 @@ var PipelineOrchestrator = class {
|
|
|
5246
5760
|
title,
|
|
5247
5761
|
description
|
|
5248
5762
|
});
|
|
5249
|
-
|
|
5763
|
+
logger20.info("Merge request created successfully", {
|
|
5250
5764
|
iid: issue.iid,
|
|
5251
5765
|
mrIid: mr.iid,
|
|
5252
5766
|
mrUrl: mr.web_url
|
|
@@ -5254,7 +5768,7 @@ var PipelineOrchestrator = class {
|
|
|
5254
5768
|
return { url: mr.web_url, iid: mr.iid };
|
|
5255
5769
|
} catch (err) {
|
|
5256
5770
|
const errorMsg = err.message;
|
|
5257
|
-
|
|
5771
|
+
logger20.warn("Failed to create merge request, trying to find existing one", {
|
|
5258
5772
|
iid: issue.iid,
|
|
5259
5773
|
error: errorMsg
|
|
5260
5774
|
});
|
|
@@ -5271,7 +5785,7 @@ var PipelineOrchestrator = class {
|
|
|
5271
5785
|
this.config.project.baseBranch
|
|
5272
5786
|
);
|
|
5273
5787
|
if (existing) {
|
|
5274
|
-
|
|
5788
|
+
logger20.info("Found existing merge request", {
|
|
5275
5789
|
iid: issueIid,
|
|
5276
5790
|
mrIid: existing.iid,
|
|
5277
5791
|
mrUrl: existing.web_url
|
|
@@ -5279,7 +5793,7 @@ var PipelineOrchestrator = class {
|
|
|
5279
5793
|
return { url: existing.web_url, iid: existing.iid };
|
|
5280
5794
|
}
|
|
5281
5795
|
} catch (findErr) {
|
|
5282
|
-
|
|
5796
|
+
logger20.warn("Failed to find existing merge request", {
|
|
5283
5797
|
iid: issueIid,
|
|
5284
5798
|
error: findErr.message
|
|
5285
5799
|
});
|
|
@@ -5324,7 +5838,7 @@ var PipelineOrchestrator = class {
|
|
|
5324
5838
|
});
|
|
5325
5839
|
return ports;
|
|
5326
5840
|
} catch (err) {
|
|
5327
|
-
|
|
5841
|
+
logger20.error("Failed to start preview servers", {
|
|
5328
5842
|
iid: issue.iid,
|
|
5329
5843
|
error: err.message
|
|
5330
5844
|
});
|
|
@@ -5359,7 +5873,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5359
5873
|
await this.mainGitMutex.runExclusive(async () => {
|
|
5360
5874
|
await this.cleanupWorktree(wtCtx);
|
|
5361
5875
|
});
|
|
5362
|
-
|
|
5876
|
+
logger20.info("Preview stopped and worktree cleaned", { iid: issueIid });
|
|
5363
5877
|
}
|
|
5364
5878
|
async markDeployed(issueIid) {
|
|
5365
5879
|
const record = this.tracker.get(issueIid);
|
|
@@ -5376,7 +5890,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5376
5890
|
try {
|
|
5377
5891
|
await this.gongfeng.closeIssue(externalId);
|
|
5378
5892
|
} catch (err) {
|
|
5379
|
-
|
|
5893
|
+
logger20.warn("Failed to close issue on Gongfeng", { iid: issueIid, error: err.message });
|
|
5380
5894
|
}
|
|
5381
5895
|
try {
|
|
5382
5896
|
const issue = await this.gongfeng.getIssueDetail(externalId);
|
|
@@ -5384,10 +5898,10 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5384
5898
|
labels.push("auto-finish:deployed");
|
|
5385
5899
|
await this.gongfeng.updateIssueLabels(externalId, labels);
|
|
5386
5900
|
} catch (err) {
|
|
5387
|
-
|
|
5901
|
+
logger20.warn("Failed to update labels", { iid: issueIid, error: err.message });
|
|
5388
5902
|
}
|
|
5389
5903
|
this.tracker.updateState(issueIid, "deployed" /* Deployed */);
|
|
5390
|
-
|
|
5904
|
+
logger20.info("Issue marked as deployed", { iid: issueIid });
|
|
5391
5905
|
}
|
|
5392
5906
|
async restartPreview(issueIid) {
|
|
5393
5907
|
const record = this.tracker.get(issueIid);
|
|
@@ -5414,7 +5928,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5414
5928
|
throw err;
|
|
5415
5929
|
}
|
|
5416
5930
|
const url = this.buildPreviewUrl(issueIid);
|
|
5417
|
-
|
|
5931
|
+
logger20.info("Preview restarted", { iid: issueIid, url });
|
|
5418
5932
|
return url;
|
|
5419
5933
|
}
|
|
5420
5934
|
getPreviewHost() {
|
|
@@ -5447,7 +5961,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5447
5961
|
if (!record) throw new IssueNotFoundError(issueIid);
|
|
5448
5962
|
const baseBranch = this.config.project.baseBranch;
|
|
5449
5963
|
const branchName = record.branchName;
|
|
5450
|
-
|
|
5964
|
+
logger20.info("Starting conflict resolution", { issueIid, branchName, baseBranch });
|
|
5451
5965
|
this.tracker.updateState(issueIid, "resolving_conflict" /* ResolvingConflict */);
|
|
5452
5966
|
this.eventBus.emitTyped("conflict:started", { issueIid });
|
|
5453
5967
|
try {
|
|
@@ -5480,7 +5994,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5480
5994
|
});
|
|
5481
5995
|
}
|
|
5482
5996
|
});
|
|
5483
|
-
|
|
5997
|
+
logger20.info("Running verification after conflict resolution", { issueIid });
|
|
5484
5998
|
const wtPlan = new PlanPersistence(wtCtx.workDir, issueIid);
|
|
5485
5999
|
wtPlan.ensureDir();
|
|
5486
6000
|
const verifyPhase = createPhase("verify", this.aiRunner, wtGit, wtPlan, this.config);
|
|
@@ -5518,10 +6032,10 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5518
6032
|
} catch {
|
|
5519
6033
|
}
|
|
5520
6034
|
await this.commentOnMr(record.mrUrl, t("conflict.mrResolvedComment"));
|
|
5521
|
-
|
|
6035
|
+
logger20.info("Conflict resolution completed", { issueIid });
|
|
5522
6036
|
} catch (err) {
|
|
5523
6037
|
const errorMsg = err.message;
|
|
5524
|
-
|
|
6038
|
+
logger20.error("Conflict resolution failed", { issueIid, error: errorMsg });
|
|
5525
6039
|
try {
|
|
5526
6040
|
const wtGit = new GitOperations(wtCtx.gitRootDir);
|
|
5527
6041
|
if (await wtGit.isRebaseInProgress()) {
|
|
@@ -5551,7 +6065,7 @@ E2E \u6D4B\u8BD5\u5C06\u5C1D\u8BD5\u4F7F\u7528 config.json \u4E2D\u7684\u9ED8\u8
|
|
|
5551
6065
|
try {
|
|
5552
6066
|
await this.gongfeng.createMergeRequestNote(mrIid, body);
|
|
5553
6067
|
} catch (err) {
|
|
5554
|
-
|
|
6068
|
+
logger20.warn("Failed to comment on MR", { mrIid, error: err.message });
|
|
5555
6069
|
}
|
|
5556
6070
|
}
|
|
5557
6071
|
};
|
|
@@ -5627,7 +6141,7 @@ ${questions}
|
|
|
5627
6141
|
}
|
|
5628
6142
|
|
|
5629
6143
|
// src/services/BrainstormService.ts
|
|
5630
|
-
var
|
|
6144
|
+
var logger21 = logger.child("Brainstorm");
|
|
5631
6145
|
function agentConfigToAIConfig(agentCfg, timeoutMs) {
|
|
5632
6146
|
return {
|
|
5633
6147
|
mode: agentCfg.mode,
|
|
@@ -5663,7 +6177,7 @@ var BrainstormService = class {
|
|
|
5663
6177
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5664
6178
|
};
|
|
5665
6179
|
this.sessions.set(session.id, session);
|
|
5666
|
-
|
|
6180
|
+
logger21.info("Created brainstorm session", { sessionId: session.id });
|
|
5667
6181
|
return session;
|
|
5668
6182
|
}
|
|
5669
6183
|
getSession(id) {
|
|
@@ -5672,7 +6186,7 @@ var BrainstormService = class {
|
|
|
5672
6186
|
async generate(sessionId, onEvent) {
|
|
5673
6187
|
const session = this.requireSession(sessionId);
|
|
5674
6188
|
session.status = "generating";
|
|
5675
|
-
|
|
6189
|
+
logger21.info("Generating SDD", { sessionId });
|
|
5676
6190
|
const prompt = buildGeneratePrompt(session.transcript);
|
|
5677
6191
|
const result = await this.generatorRunner.run({
|
|
5678
6192
|
prompt,
|
|
@@ -5698,7 +6212,7 @@ var BrainstormService = class {
|
|
|
5698
6212
|
const session = this.requireSession(sessionId);
|
|
5699
6213
|
const roundNum = session.rounds.length + 1;
|
|
5700
6214
|
session.status = "reviewing";
|
|
5701
|
-
|
|
6215
|
+
logger21.info("Reviewing SDD", { sessionId, round: roundNum });
|
|
5702
6216
|
onEvent?.({ type: "round:start", data: { round: roundNum, phase: "review" }, round: roundNum });
|
|
5703
6217
|
const prompt = buildReviewPrompt(session.currentSdd, roundNum);
|
|
5704
6218
|
const result = await this.reviewerRunner.run({
|
|
@@ -5731,7 +6245,7 @@ var BrainstormService = class {
|
|
|
5731
6245
|
throw new Error("No review round to refine from");
|
|
5732
6246
|
}
|
|
5733
6247
|
session.status = "refining";
|
|
5734
|
-
|
|
6248
|
+
logger21.info("Refining SDD", { sessionId, round: currentRound.round });
|
|
5735
6249
|
const prompt = buildRefinePrompt(currentRound.questions);
|
|
5736
6250
|
const result = await this.generatorRunner.run({
|
|
5737
6251
|
prompt,
|
|
@@ -5818,4 +6332,4 @@ export {
|
|
|
5818
6332
|
PipelineOrchestrator,
|
|
5819
6333
|
BrainstormService
|
|
5820
6334
|
};
|
|
5821
|
-
//# sourceMappingURL=chunk-
|
|
6335
|
+
//# sourceMappingURL=chunk-LDGK5NMS.js.map
|