@xdevops/issue-auto-finish 1.0.1 → 1.0.3
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/KnowledgeAnalyzer-EZSJT2MJ.js +13 -0
- package/dist/KnowledgeAnalyzer-EZSJT2MJ.js.map +1 -0
- package/dist/KnowledgeStore-4ROC6F56.js +10 -0
- package/dist/KnowledgeStore-4ROC6F56.js.map +1 -0
- package/dist/ai-runner/AIRunner.d.ts +2 -0
- package/dist/ai-runner/AIRunner.d.ts.map +1 -1
- package/dist/ai-runner/BaseAIRunner.d.ts +9 -0
- package/dist/ai-runner/BaseAIRunner.d.ts.map +1 -1
- package/dist/ai-runner-RGAJPOOW.js +16 -0
- package/dist/ai-runner-RGAJPOOW.js.map +1 -0
- package/dist/analyze-I7UOJB4F.js +72 -0
- package/dist/analyze-I7UOJB4F.js.map +1 -0
- package/dist/chunk-3JUHZGX5.js +171 -0
- package/dist/chunk-3JUHZGX5.js.map +1 -0
- package/dist/chunk-5JYCGAU3.js +318 -0
- package/dist/chunk-5JYCGAU3.js.map +1 -0
- package/dist/chunk-5VUB3UUK.js +643 -0
- package/dist/chunk-5VUB3UUK.js.map +1 -0
- package/dist/{chunk-IDUKWCC2.js → chunk-C6ZJVIPZ.js} +1151 -80
- package/dist/chunk-C6ZJVIPZ.js.map +1 -0
- package/dist/{chunk-OWVT3Z34.js → chunk-JFYAXNNS.js} +121 -31
- package/dist/chunk-JFYAXNNS.js.map +1 -0
- package/dist/chunk-KISVPNSV.js +188 -0
- package/dist/chunk-KISVPNSV.js.map +1 -0
- package/dist/{chunk-I3T573SU.js → chunk-LEQYGOMJ.js} +65 -2
- package/dist/chunk-LEQYGOMJ.js.map +1 -0
- package/dist/{chunk-TBIEB3JY.js → chunk-N5YK6YVI.js} +592 -767
- package/dist/chunk-N5YK6YVI.js.map +1 -0
- package/dist/{chunk-RIUI4ROA.js → chunk-PECYMYAK.js} +2 -2
- package/dist/chunk-SWG2Y7YX.js +410 -0
- package/dist/chunk-SWG2Y7YX.js.map +1 -0
- package/dist/chunk-TZ6C7HL5.js +59 -0
- package/dist/chunk-TZ6C7HL5.js.map +1 -0
- package/dist/cli/commands/analyze.d.ts +8 -0
- package/dist/cli/commands/analyze.d.ts.map +1 -0
- package/dist/cli.js +67 -3
- package/dist/cli.js.map +1 -1
- package/dist/clients/GongfengClient.d.ts +5 -0
- package/dist/clients/GongfengClient.d.ts.map +1 -1
- package/dist/config-RI7NLDXI.js +7 -0
- package/dist/config-RI7NLDXI.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/{doctor-B26Q6JWI.js → doctor-ZPGIBA5N.js} +3 -3
- package/dist/events/EventBus.d.ts +1 -1
- package/dist/events/EventBus.d.ts.map +1 -1
- package/dist/git/GitOperations.d.ts +12 -0
- package/dist/git/GitOperations.d.ts.map +1 -1
- package/dist/i18n/locales/en.d.ts.map +1 -1
- package/dist/i18n/locales/zh-CN.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -5
- package/dist/{init-L3VIWCOV.js → init-LZGCIHE7.js} +8 -4
- package/dist/{init-L3VIWCOV.js.map → init-LZGCIHE7.js.map} +1 -1
- package/dist/knowledge/KnowledgeAnalyzer.d.ts +31 -0
- package/dist/knowledge/KnowledgeAnalyzer.d.ts.map +1 -0
- package/dist/knowledge/KnowledgeDefaults.d.ts +7 -0
- package/dist/knowledge/KnowledgeDefaults.d.ts.map +1 -0
- package/dist/knowledge/KnowledgeEntry.d.ts +30 -0
- package/dist/knowledge/KnowledgeEntry.d.ts.map +1 -0
- package/dist/knowledge/KnowledgeLoader.d.ts +18 -0
- package/dist/knowledge/KnowledgeLoader.d.ts.map +1 -0
- package/dist/knowledge/KnowledgeStore.d.ts +35 -0
- package/dist/knowledge/KnowledgeStore.d.ts.map +1 -0
- package/dist/knowledge/ProjectKnowledge.d.ts +79 -0
- package/dist/knowledge/ProjectKnowledge.d.ts.map +1 -0
- package/dist/knowledge/analyze-prompt.d.ts +2 -0
- package/dist/knowledge/analyze-prompt.d.ts.map +1 -0
- package/dist/knowledge/importers/GongfengExtractor.d.ts +27 -0
- package/dist/knowledge/importers/GongfengExtractor.d.ts.map +1 -0
- package/dist/knowledge/importers/IwikiImporter.d.ts +21 -0
- package/dist/knowledge/importers/IwikiImporter.d.ts.map +1 -0
- package/dist/knowledge/index.d.ts +12 -0
- package/dist/knowledge/index.d.ts.map +1 -0
- package/dist/lib.js +19 -10
- package/dist/orchestrator/PipelineOrchestrator.d.ts +5 -1
- package/dist/orchestrator/PipelineOrchestrator.d.ts.map +1 -1
- package/dist/phases/BasePhase.d.ts.map +1 -1
- package/dist/poller/IssuePoller.d.ts +5 -0
- package/dist/poller/IssuePoller.d.ts.map +1 -1
- package/dist/prompts/chat-templates.d.ts +4 -0
- package/dist/prompts/chat-templates.d.ts.map +1 -0
- package/dist/prompts/templates.d.ts +11 -0
- package/dist/prompts/templates.d.ts.map +1 -1
- package/dist/rules/RuleResolver.d.ts +4 -0
- package/dist/rules/RuleResolver.d.ts.map +1 -1
- package/dist/run.js +11 -5
- package/dist/run.js.map +1 -1
- package/dist/services/ChatService.d.ts +39 -0
- package/dist/services/ChatService.d.ts.map +1 -0
- package/dist/shutdown/ShutdownSignal.d.ts +3 -0
- package/dist/shutdown/ShutdownSignal.d.ts.map +1 -0
- package/dist/{start-TVN4SS6E.js → start-NMQHUKGF.js} +1 -1
- package/dist/tracker/IssueState.d.ts +1 -0
- package/dist/tracker/IssueState.d.ts.map +1 -1
- package/dist/tracker/IssueTracker.d.ts +2 -0
- package/dist/tracker/IssueTracker.d.ts.map +1 -1
- package/dist/updater/AutoUpdater.d.ts +33 -0
- package/dist/updater/AutoUpdater.d.ts.map +1 -0
- package/dist/updater/UpdateExecutor.d.ts +7 -0
- package/dist/updater/UpdateExecutor.d.ts.map +1 -0
- package/dist/updater/VersionChecker.d.ts +22 -0
- package/dist/updater/VersionChecker.d.ts.map +1 -0
- package/dist/web/WebServer.d.ts +4 -0
- package/dist/web/WebServer.d.ts.map +1 -1
- package/dist/web/routes/api.d.ts +4 -0
- package/dist/web/routes/api.d.ts.map +1 -1
- package/dist/web/routes/chat.d.ts +7 -0
- package/dist/web/routes/chat.d.ts.map +1 -0
- package/dist/web/routes/knowledge.d.ts +13 -0
- package/dist/web/routes/knowledge.d.ts.map +1 -0
- package/dist/web/routes/setup.d.ts.map +1 -1
- package/dist/webhook/CommandExecutor.d.ts +4 -0
- package/dist/webhook/CommandExecutor.d.ts.map +1 -1
- package/dist/webhook/CommandParser.d.ts +2 -2
- package/dist/webhook/CommandParser.d.ts.map +1 -1
- package/dist/webhook/WebhookHandler.d.ts +8 -0
- package/dist/webhook/WebhookHandler.d.ts.map +1 -1
- package/dist/webhook/WebhookServer.d.ts +2 -0
- package/dist/webhook/WebhookServer.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/web/frontend/dist/assets/index-AcJ0lPIv.js +67 -0
- package/src/web/frontend/dist/assets/index-BbRt5BAr.css +1 -0
- package/src/web/frontend/dist/index.html +2 -2
- package/dist/chunk-I3T573SU.js.map +0 -1
- package/dist/chunk-IDUKWCC2.js.map +0 -1
- package/dist/chunk-OWVT3Z34.js.map +0 -1
- package/dist/chunk-TBIEB3JY.js.map +0 -1
- package/src/web/frontend/dist/assets/index-CQdlU9PE.js +0 -65
- package/src/web/frontend/dist/assets/index-CgMEkyZJ.css +0 -1
- /package/dist/{chunk-RIUI4ROA.js.map → chunk-PECYMYAK.js.map} +0 -0
- /package/dist/{doctor-B26Q6JWI.js.map → doctor-ZPGIBA5N.js.map} +0 -0
- /package/dist/{start-TVN4SS6E.js.map → start-NMQHUKGF.js.map} +0 -0
|
@@ -1,220 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getProjectKnowledge
|
|
3
|
+
} from "./chunk-3JUHZGX5.js";
|
|
1
4
|
import {
|
|
2
5
|
t
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const localEnv = path.resolve(__dirname, "../.env");
|
|
15
|
-
if (fs.existsSync(localEnv)) return localEnv;
|
|
16
|
-
const cwdEnv = path.resolve(process.cwd(), ".env");
|
|
17
|
-
if (fs.existsSync(cwdEnv)) return cwdEnv;
|
|
18
|
-
const globalEnv = path.join(os.homedir(), ".issue-auto-finish", ".env");
|
|
19
|
-
if (fs.existsSync(globalEnv)) return globalEnv;
|
|
20
|
-
return localEnv;
|
|
21
|
-
}
|
|
22
|
-
var _dotenvLoaded = false;
|
|
23
|
-
function ensureDotenvLoaded() {
|
|
24
|
-
if (_dotenvLoaded) return;
|
|
25
|
-
_dotenvLoaded = true;
|
|
26
|
-
loadDotenv({ path: resolveEnvPath() });
|
|
27
|
-
}
|
|
28
|
-
var DEFAULT_AI_MODEL = "Claude-4.6-Opus";
|
|
29
|
-
function requireEnv(key) {
|
|
30
|
-
const val = process.env[key];
|
|
31
|
-
if (!val) {
|
|
32
|
-
throw new Error(`Missing required environment variable: ${key}`);
|
|
33
|
-
}
|
|
34
|
-
return val;
|
|
35
|
-
}
|
|
36
|
-
function optionalEnv(key, defaultValue) {
|
|
37
|
-
return process.env[key] || defaultValue;
|
|
38
|
-
}
|
|
39
|
-
function loadConfig() {
|
|
40
|
-
ensureDotenvLoaded();
|
|
41
|
-
return {
|
|
42
|
-
gongfeng: {
|
|
43
|
-
apiUrl: requireEnv("GONGFENG_API_URL"),
|
|
44
|
-
privateToken: requireEnv("GONGFENG_PRIVATE_TOKEN"),
|
|
45
|
-
projectPath: requireEnv("GONGFENG_PROJECT_PATH")
|
|
46
|
-
},
|
|
47
|
-
project: {
|
|
48
|
-
workDir: requireEnv("PROJECT_WORK_DIR"),
|
|
49
|
-
gitRootDir: optionalEnv("GIT_ROOT_DIR", requireEnv("PROJECT_WORK_DIR")),
|
|
50
|
-
baseBranch: optionalEnv("BASE_BRANCH", "master"),
|
|
51
|
-
branchPrefix: optionalEnv("BRANCH_PREFIX", "feat/issue"),
|
|
52
|
-
worktreeBaseDir: optionalEnv("WORKTREE_BASE_DIR", ""),
|
|
53
|
-
projectSubDir: optionalEnv("PROJECT_SUBDIR", "")
|
|
54
|
-
},
|
|
55
|
-
claude: {
|
|
56
|
-
binary: optionalEnv("CLAUDE_BINARY", "claude-internal"),
|
|
57
|
-
phaseTimeoutMs: parseInt(optionalEnv("CLAUDE_PHASE_TIMEOUT_MS", "1800000"), 10),
|
|
58
|
-
nvmNodeVersion: optionalEnv("NVM_NODE_VERSION", "20")
|
|
59
|
-
},
|
|
60
|
-
ai: buildAIConfig(),
|
|
61
|
-
poll: {
|
|
62
|
-
intervalMs: parseInt(optionalEnv("POLL_INTERVAL_MS", "60000"), 10),
|
|
63
|
-
discoveryIntervalMs: parseInt(
|
|
64
|
-
optionalEnv("POLL_DISCOVERY_INTERVAL_MS", optionalEnv("POLL_INTERVAL_MS", "60000")),
|
|
65
|
-
10
|
|
66
|
-
),
|
|
67
|
-
driveIntervalMs: parseInt(optionalEnv("POLL_DRIVE_INTERVAL_MS", "15000"), 10),
|
|
68
|
-
maxRetries: parseInt(optionalEnv("MAX_RETRIES", "3"), 10),
|
|
69
|
-
maxConcurrent: parseInt(optionalEnv("MAX_CONCURRENT_ISSUES", "3"), 10)
|
|
70
|
-
},
|
|
71
|
-
pipeline: {
|
|
72
|
-
mode: optionalEnv("PIPELINE_MODE", "auto")
|
|
73
|
-
},
|
|
74
|
-
review: {
|
|
75
|
-
enabled: optionalEnv("REVIEW_ENABLED", "true") === "true",
|
|
76
|
-
autoApproveLabels: optionalEnv("REVIEW_AUTO_APPROVE_LABELS", "").split(",").map((s) => s.trim()).filter(Boolean)
|
|
77
|
-
},
|
|
78
|
-
web: {
|
|
79
|
-
enabled: optionalEnv("WEB_ENABLED", "true") === "true",
|
|
80
|
-
port: parseInt(optionalEnv("WEB_PORT", "3000"), 10),
|
|
81
|
-
frontendDistDir: optionalEnv(
|
|
82
|
-
"FRONTEND_DIST_DIR",
|
|
83
|
-
path.resolve(process.cwd(), "src/web/frontend/dist")
|
|
84
|
-
)
|
|
85
|
-
},
|
|
86
|
-
issueNoteSync: {
|
|
87
|
-
enabled: optionalEnv("ISSUE_NOTE_SYNC_ENABLED", "true") === "true",
|
|
88
|
-
webBaseUrl: optionalEnv(
|
|
89
|
-
"WEB_BASE_URL",
|
|
90
|
-
`http://localhost:${optionalEnv("WEB_PORT", "3000")}`
|
|
91
|
-
)
|
|
92
|
-
},
|
|
93
|
-
webhook: {
|
|
94
|
-
enabled: optionalEnv("WEBHOOK_ENABLED", "false") === "true",
|
|
95
|
-
port: parseInt(optionalEnv("WEBHOOK_PORT", "8081"), 10),
|
|
96
|
-
secret: optionalEnv("WEBHOOK_SECRET", ""),
|
|
97
|
-
llmFallback: optionalEnv("WEBHOOK_LLM_FALLBACK", "true") === "true",
|
|
98
|
-
llmBinary: optionalEnv("WEBHOOK_LLM_BINARY", "claude-internal")
|
|
99
|
-
},
|
|
100
|
-
e2e: {
|
|
101
|
-
enabled: optionalEnv("E2E_UI_ENABLED", "false") === "true",
|
|
102
|
-
baseUrl: optionalEnv("E2E_BASE_URL", "https://localhost:8890"),
|
|
103
|
-
backendUrl: optionalEnv("E2E_BACKEND_URL", "http://127.0.0.1:3000"),
|
|
104
|
-
authCookies: optionalEnv("E2E_AUTH_COOKIES", "[]"),
|
|
105
|
-
backendPortBase: parseInt(optionalEnv("E2E_BACKEND_PORT_BASE", "4000"), 10),
|
|
106
|
-
frontendPortBase: parseInt(optionalEnv("E2E_FRONTEND_PORT_BASE", "9000"), 10)
|
|
107
|
-
},
|
|
108
|
-
preview: {
|
|
109
|
-
enabled: optionalEnv("PREVIEW_ENABLED", "false") === "true",
|
|
110
|
-
host: optionalEnv("PREVIEW_HOST", ""),
|
|
111
|
-
ttlMs: parseInt(optionalEnv("PREVIEW_TTL_MS", String(24 * 60 * 60 * 1e3)), 10),
|
|
112
|
-
keepAfterComplete: optionalEnv("PREVIEW_KEEP_AFTER_COMPLETE", "true") === "true"
|
|
113
|
-
},
|
|
114
|
-
brainstorm: {
|
|
115
|
-
enabled: optionalEnv("BRAINSTORM_ENABLED", "true") === "true",
|
|
116
|
-
maxRefinementRounds: parseInt(optionalEnv("BRAINSTORM_MAX_ROUNDS", "5"), 10),
|
|
117
|
-
timeoutMs: parseInt(optionalEnv("BRAINSTORM_TIMEOUT_MS", "600000"), 10),
|
|
118
|
-
generator: buildBrainstormAgentConfig("GENERATOR"),
|
|
119
|
-
reviewer: buildBrainstormAgentConfig("REVIEWER")
|
|
120
|
-
},
|
|
121
|
-
locale: optionalEnv("LOCALE", "zh-CN") === "en" ? "en" : "zh-CN"
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
function resolveAIRunnerMode(raw) {
|
|
125
|
-
if (raw === "cursor-agent") return "cursor-agent";
|
|
126
|
-
if (raw === "codebuddy") return "codebuddy";
|
|
127
|
-
return "claude-internal";
|
|
128
|
-
}
|
|
129
|
-
function resolveAIBinary(mode) {
|
|
130
|
-
switch (mode) {
|
|
131
|
-
case "cursor-agent":
|
|
132
|
-
return optionalEnv("CURSOR_BINARY", "cursor");
|
|
133
|
-
case "codebuddy":
|
|
134
|
-
return optionalEnv("CODEBUDDY_BINARY", "codebuddy");
|
|
135
|
-
default:
|
|
136
|
-
return optionalEnv("CLAUDE_BINARY", "claude-internal");
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
function buildAIConfig() {
|
|
140
|
-
const mode = resolveAIRunnerMode(optionalEnv("AI_RUNNER_MODE", "claude-internal"));
|
|
141
|
-
return {
|
|
142
|
-
mode,
|
|
143
|
-
binary: resolveAIBinary(mode),
|
|
144
|
-
phaseTimeoutMs: parseInt(optionalEnv("CLAUDE_PHASE_TIMEOUT_MS", "1800000"), 10),
|
|
145
|
-
nvmNodeVersion: optionalEnv("NVM_NODE_VERSION", "20"),
|
|
146
|
-
model: optionalEnv("AI_MODEL", DEFAULT_AI_MODEL)
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
function buildBrainstormAgentConfig(role) {
|
|
150
|
-
const globalMode = optionalEnv("AI_RUNNER_MODE", "claude-internal");
|
|
151
|
-
const roleMode = optionalEnv(`BRAINSTORM_${role}_MODE`, globalMode);
|
|
152
|
-
const mode = resolveAIRunnerMode(roleMode);
|
|
153
|
-
return {
|
|
154
|
-
mode,
|
|
155
|
-
binary: optionalEnv(`BRAINSTORM_${role}_BINARY`, resolveAIBinary(mode)),
|
|
156
|
-
nvmNodeVersion: optionalEnv("NVM_NODE_VERSION", "20"),
|
|
157
|
-
model: process.env[`BRAINSTORM_${role}_MODEL`] || optionalEnv("AI_MODEL", DEFAULT_AI_MODEL)
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// src/logger.ts
|
|
162
|
-
var LOG_LEVELS = {
|
|
163
|
-
debug: 0,
|
|
164
|
-
info: 1,
|
|
165
|
-
warn: 2,
|
|
166
|
-
error: 3
|
|
167
|
-
};
|
|
168
|
-
var Logger = class _Logger {
|
|
169
|
-
level = "info";
|
|
170
|
-
context;
|
|
171
|
-
constructor(context) {
|
|
172
|
-
this.context = context;
|
|
173
|
-
const envLevel = process.env.LOG_LEVEL;
|
|
174
|
-
if (envLevel && envLevel in LOG_LEVELS) {
|
|
175
|
-
this.level = envLevel;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
child(context) {
|
|
179
|
-
const child = new _Logger(this.context ? `${this.context}:${context}` : context);
|
|
180
|
-
child.level = this.level;
|
|
181
|
-
return child;
|
|
182
|
-
}
|
|
183
|
-
format(level, message, meta) {
|
|
184
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
185
|
-
const prefix = this.context ? `[${this.context}]` : "";
|
|
186
|
-
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
|
|
187
|
-
return `${ts} ${level.toUpperCase().padEnd(5)} ${prefix} ${message}${metaStr}`;
|
|
188
|
-
}
|
|
189
|
-
log(level, message, meta) {
|
|
190
|
-
if (LOG_LEVELS[level] < LOG_LEVELS[this.level]) return;
|
|
191
|
-
const line = this.format(level, message, meta);
|
|
192
|
-
if (level === "error") {
|
|
193
|
-
console.error(line);
|
|
194
|
-
} else if (level === "warn") {
|
|
195
|
-
console.warn(line);
|
|
196
|
-
} else {
|
|
197
|
-
console.log(line);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
debug(message, meta) {
|
|
201
|
-
this.log("debug", message, meta);
|
|
202
|
-
}
|
|
203
|
-
info(message, meta) {
|
|
204
|
-
this.log("info", message, meta);
|
|
205
|
-
}
|
|
206
|
-
warn(message, meta) {
|
|
207
|
-
this.log("warn", message, meta);
|
|
208
|
-
}
|
|
209
|
-
error(message, meta) {
|
|
210
|
-
this.log("error", message, meta);
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
var logger = new Logger();
|
|
6
|
+
} from "./chunk-JFYAXNNS.js";
|
|
7
|
+
import {
|
|
8
|
+
KNOWLEDGE_DEFAULTS
|
|
9
|
+
} from "./chunk-5VUB3UUK.js";
|
|
10
|
+
import {
|
|
11
|
+
createAIRunner,
|
|
12
|
+
isShuttingDown
|
|
13
|
+
} from "./chunk-SWG2Y7YX.js";
|
|
14
|
+
import {
|
|
15
|
+
logger
|
|
16
|
+
} from "./chunk-TZ6C7HL5.js";
|
|
214
17
|
|
|
215
18
|
// src/clients/GongfengClient.ts
|
|
216
|
-
import
|
|
217
|
-
import
|
|
19
|
+
import fs from "fs";
|
|
20
|
+
import path from "path";
|
|
218
21
|
var logger2 = logger.child("GongfengClient");
|
|
219
22
|
var AGENT_NOTE_MARKER = "\n\n<!-- issue-auto-finish-agent -->";
|
|
220
23
|
var AGENT_NOTE_MARKER_PATTERN = "<!-- issue-auto-finish-agent -->";
|
|
@@ -231,8 +34,8 @@ var GongfengClient = class {
|
|
|
231
34
|
const encoded = encodeURIComponent(this.projectPath);
|
|
232
35
|
return `${this.apiUrl}/api/v3/projects/${encoded}`;
|
|
233
36
|
}
|
|
234
|
-
async requestRaw(
|
|
235
|
-
const url = `${this.projectApiBase}${
|
|
37
|
+
async requestRaw(path10, options = {}) {
|
|
38
|
+
const url = `${this.projectApiBase}${path10}`;
|
|
236
39
|
logger2.debug("API request", { method: options.method || "GET", url });
|
|
237
40
|
const resp = await fetch(url, {
|
|
238
41
|
...options,
|
|
@@ -248,8 +51,8 @@ var GongfengClient = class {
|
|
|
248
51
|
}
|
|
249
52
|
return resp;
|
|
250
53
|
}
|
|
251
|
-
async request(
|
|
252
|
-
const resp = await this.requestRaw(
|
|
54
|
+
async request(path10, options = {}) {
|
|
55
|
+
const resp = await this.requestRaw(path10, options);
|
|
253
56
|
return resp.json();
|
|
254
57
|
}
|
|
255
58
|
async createIssue(title, description, labels) {
|
|
@@ -342,8 +145,8 @@ var GongfengClient = class {
|
|
|
342
145
|
return `${this.apiUrl}/${this.projectPath}/merge_requests/${mrIid}`;
|
|
343
146
|
}
|
|
344
147
|
async uploadFile(filePath) {
|
|
345
|
-
const fileData =
|
|
346
|
-
const fileName =
|
|
148
|
+
const fileData = fs.readFileSync(filePath);
|
|
149
|
+
const fileName = path.basename(filePath);
|
|
347
150
|
const mimeType = fileName.endsWith(".png") ? "image/png" : "application/octet-stream";
|
|
348
151
|
const blob = new Blob([fileData], { type: mimeType });
|
|
349
152
|
const formData = new FormData();
|
|
@@ -418,6 +221,13 @@ var GongfengClient = class {
|
|
|
418
221
|
}
|
|
419
222
|
return agentNotes.length;
|
|
420
223
|
}
|
|
224
|
+
async getMergeRequestDetail(mrIid) {
|
|
225
|
+
const mr = await this.request(`/merge_requests/${mrIid}`);
|
|
226
|
+
if (!mr.web_url && mr.iid) {
|
|
227
|
+
mr.web_url = this.buildMergeRequestUrl(mr.iid);
|
|
228
|
+
}
|
|
229
|
+
return mr;
|
|
230
|
+
}
|
|
421
231
|
async addLabel(issueId, label) {
|
|
422
232
|
const issue = await this.getIssueDetail(issueId);
|
|
423
233
|
if (issue.labels.includes(label)) {
|
|
@@ -556,319 +366,51 @@ var GitOperations = class {
|
|
|
556
366
|
return null;
|
|
557
367
|
}
|
|
558
368
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
var logger4 = logger.child("AIRunner");
|
|
564
|
-
var BaseAIRunner = class {
|
|
565
|
-
async run(options) {
|
|
566
|
-
const { prompt, workDir, timeoutMs, sessionId, continueSession, onStreamEvent } = options;
|
|
567
|
-
logger4.info("Running AI runner", {
|
|
568
|
-
workDir,
|
|
569
|
-
timeoutMs,
|
|
570
|
-
continueSession: !!continueSession,
|
|
571
|
-
sessionId
|
|
572
|
-
});
|
|
573
|
-
return new Promise((resolve) => {
|
|
574
|
-
const chunks = [];
|
|
575
|
-
const stderrChunks = [];
|
|
576
|
-
let timedOut = false;
|
|
577
|
-
let lineBuffer = "";
|
|
578
|
-
const binary = this.getBinary();
|
|
579
|
-
const args = this.buildArgs(options);
|
|
580
|
-
const spawnOpts = this.getSpawnOptions(options);
|
|
581
|
-
const child = spawn(binary, args, {
|
|
582
|
-
...spawnOpts,
|
|
583
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
584
|
-
});
|
|
585
|
-
const timer = setTimeout(() => {
|
|
586
|
-
timedOut = true;
|
|
587
|
-
child.kill("SIGTERM");
|
|
588
|
-
logger4.warn("AI runner timed out", { timeoutMs });
|
|
589
|
-
}, timeoutMs);
|
|
590
|
-
const flushInterval = onStreamEvent ? setInterval(() => {
|
|
591
|
-
if (lineBuffer.trim()) {
|
|
592
|
-
onStreamEvent({ type: "partial", content: lineBuffer.trim(), timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
593
|
-
}
|
|
594
|
-
}, 3e3) : void 0;
|
|
595
|
-
child.stdout.on("data", (chunk) => {
|
|
596
|
-
chunks.push(chunk);
|
|
597
|
-
if (onStreamEvent) {
|
|
598
|
-
lineBuffer += chunk.toString();
|
|
599
|
-
let newlineIdx;
|
|
600
|
-
while ((newlineIdx = lineBuffer.indexOf("\n")) !== -1) {
|
|
601
|
-
const line = lineBuffer.slice(0, newlineIdx).trim();
|
|
602
|
-
lineBuffer = lineBuffer.slice(newlineIdx + 1);
|
|
603
|
-
if (!line) continue;
|
|
604
|
-
this.emitStreamLine(line, onStreamEvent);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
});
|
|
608
|
-
child.stderr.on("data", (chunk) => {
|
|
609
|
-
stderrChunks.push(chunk);
|
|
610
|
-
const text = chunk.toString();
|
|
611
|
-
logger4.debug("AI runner stderr: " + text);
|
|
612
|
-
if (onStreamEvent) {
|
|
613
|
-
onStreamEvent({ type: "stderr", content: text, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
child.on("close", (code) => {
|
|
617
|
-
clearTimeout(timer);
|
|
618
|
-
if (flushInterval) clearInterval(flushInterval);
|
|
619
|
-
if (onStreamEvent && lineBuffer.trim()) {
|
|
620
|
-
this.emitStreamLine(lineBuffer.trim(), onStreamEvent);
|
|
621
|
-
}
|
|
622
|
-
const rawOutput = Buffer.concat(chunks).toString("utf-8");
|
|
623
|
-
const { output, resolvedSessionId } = this.parseOutput(rawOutput, sessionId);
|
|
624
|
-
const success = code === 0 && !timedOut;
|
|
625
|
-
logger4.info("AI runner finished", { exitCode: code, success, timedOut });
|
|
626
|
-
const stderrText = stderrChunks.length > 0 ? Buffer.concat(stderrChunks).toString("utf-8").trim() : "";
|
|
627
|
-
const finalOutput = !success && !output.trim() && stderrText ? stderrText : output;
|
|
628
|
-
resolve({
|
|
629
|
-
success,
|
|
630
|
-
output: finalOutput,
|
|
631
|
-
sessionId: resolvedSessionId ?? sessionId,
|
|
632
|
-
exitCode: code
|
|
633
|
-
});
|
|
634
|
-
});
|
|
635
|
-
child.on("error", (err) => {
|
|
636
|
-
clearTimeout(timer);
|
|
637
|
-
if (flushInterval) clearInterval(flushInterval);
|
|
638
|
-
logger4.error("AI runner spawn error", { error: err.message });
|
|
639
|
-
resolve({
|
|
640
|
-
success: false,
|
|
641
|
-
output: err.message,
|
|
642
|
-
exitCode: null
|
|
643
|
-
});
|
|
644
|
-
});
|
|
645
|
-
child.stdin.write(prompt);
|
|
646
|
-
child.stdin.end();
|
|
647
|
-
});
|
|
369
|
+
async getConflictFiles() {
|
|
370
|
+
const output = await this.exec(["diff", "--name-only", "--diff-filter=U"]);
|
|
371
|
+
if (!output) return [];
|
|
372
|
+
return output.split("\n").filter(Boolean);
|
|
648
373
|
}
|
|
649
|
-
|
|
650
|
-
try {
|
|
651
|
-
const parsed = JSON.parse(line);
|
|
652
|
-
onStreamEvent({
|
|
653
|
-
type: typeof parsed.type === "string" ? parsed.type : "raw",
|
|
654
|
-
content: parsed,
|
|
655
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
656
|
-
});
|
|
657
|
-
} catch {
|
|
658
|
-
onStreamEvent({ type: "raw", content: line, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
/**
|
|
662
|
-
* Handles both stream-json (one JSON object per line) and legacy single-JSON formats.
|
|
663
|
-
*/
|
|
664
|
-
parseOutput(rawOutput, _sessionId) {
|
|
665
|
-
let output = rawOutput;
|
|
666
|
-
let resolvedSessionId;
|
|
667
|
-
const events = this.tryParseStreamJson(rawOutput);
|
|
668
|
-
if (events) {
|
|
669
|
-
output = events.filter((item) => item.type === "result").map((item) => item.result ?? "").join("\n");
|
|
670
|
-
const sessionItem = events.find(
|
|
671
|
-
(item) => item.session_id != null
|
|
672
|
-
);
|
|
673
|
-
if (sessionItem) {
|
|
674
|
-
resolvedSessionId = sessionItem.session_id;
|
|
675
|
-
}
|
|
676
|
-
if (!output) {
|
|
677
|
-
output = this.summarizeStreamEvents(events);
|
|
678
|
-
}
|
|
679
|
-
return { output, resolvedSessionId };
|
|
680
|
-
}
|
|
374
|
+
async rebase(targetRef) {
|
|
681
375
|
try {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
);
|
|
688
|
-
|
|
689
|
-
resolvedSessionId = sessionItem.session_id;
|
|
690
|
-
}
|
|
691
|
-
} else if (parsed.result != null) {
|
|
692
|
-
output = parsed.result;
|
|
693
|
-
resolvedSessionId = parsed.session_id;
|
|
694
|
-
}
|
|
695
|
-
} catch {
|
|
696
|
-
}
|
|
697
|
-
return { output, resolvedSessionId };
|
|
698
|
-
}
|
|
699
|
-
summarizeStreamEvents(events) {
|
|
700
|
-
const errorEvents = events.filter(
|
|
701
|
-
(e) => e.type === "error" || e.subtype === "error"
|
|
702
|
-
);
|
|
703
|
-
if (errorEvents.length > 0) {
|
|
704
|
-
const messages = errorEvents.map(
|
|
705
|
-
(e) => String(e.message ?? e.error ?? JSON.stringify(e))
|
|
706
|
-
);
|
|
707
|
-
return `AI runner \u9519\u8BEF: ${messages.join("; ")}`;
|
|
708
|
-
}
|
|
709
|
-
const types = events.map((e) => String(e.type ?? "unknown"));
|
|
710
|
-
const typeCounts = {};
|
|
711
|
-
for (const t2 of types) {
|
|
712
|
-
typeCounts[t2] = (typeCounts[t2] ?? 0) + 1;
|
|
713
|
-
}
|
|
714
|
-
const summary = Object.entries(typeCounts).map(([t2, c]) => `${t2}(${c})`).join(", ");
|
|
715
|
-
return `AI runner \u672A\u8FD4\u56DE\u7ED3\u679C (\u6536\u5230 ${events.length} \u4E2A\u4E8B\u4EF6: ${summary})`;
|
|
716
|
-
}
|
|
717
|
-
tryParseStreamJson(raw) {
|
|
718
|
-
const lines = raw.split("\n").filter((l) => l.trim());
|
|
719
|
-
if (lines.length < 2) return null;
|
|
720
|
-
const objects = [];
|
|
721
|
-
for (const line of lines) {
|
|
722
|
-
try {
|
|
723
|
-
objects.push(JSON.parse(line));
|
|
724
|
-
} catch {
|
|
725
|
-
return null;
|
|
376
|
+
await this.exec(["rebase", targetRef]);
|
|
377
|
+
return { success: true, conflictFiles: [] };
|
|
378
|
+
} catch (err) {
|
|
379
|
+
const msg = err.message || "";
|
|
380
|
+
if (msg.includes("CONFLICT") || msg.includes("could not apply")) {
|
|
381
|
+
const conflictFiles = await this.getConflictFiles();
|
|
382
|
+
return { success: false, conflictFiles };
|
|
726
383
|
}
|
|
384
|
+
throw err;
|
|
727
385
|
}
|
|
728
|
-
return objects;
|
|
729
|
-
}
|
|
730
|
-
};
|
|
731
|
-
|
|
732
|
-
// src/ai-runner/ClaudeInternalRunner.ts
|
|
733
|
-
var ClaudeInternalRunner = class extends BaseAIRunner {
|
|
734
|
-
binary;
|
|
735
|
-
nvmNodeVersion;
|
|
736
|
-
model;
|
|
737
|
-
constructor(binary, nvmNodeVersion, model) {
|
|
738
|
-
super();
|
|
739
|
-
this.binary = binary;
|
|
740
|
-
this.nvmNodeVersion = nvmNodeVersion;
|
|
741
|
-
this.model = model;
|
|
742
|
-
}
|
|
743
|
-
getBinary() {
|
|
744
|
-
return this.binary;
|
|
745
|
-
}
|
|
746
|
-
buildArgs(options) {
|
|
747
|
-
const args = ["-p", "-", "--output-format", "stream-json", "--verbose"];
|
|
748
|
-
if (options.mode === "plan") {
|
|
749
|
-
args.push("--permission-mode", "plan", "--allowedTools", "Read,Grep,Glob,WebSearch");
|
|
750
|
-
} else {
|
|
751
|
-
args.push("--dangerously-skip-permissions");
|
|
752
|
-
}
|
|
753
|
-
if (this.model) {
|
|
754
|
-
args.push("--model", this.model);
|
|
755
|
-
}
|
|
756
|
-
if (options.continueSession && options.sessionId) {
|
|
757
|
-
args.push("--resume", options.sessionId);
|
|
758
|
-
}
|
|
759
|
-
return args;
|
|
760
386
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
387
|
+
async rebaseContinue() {
|
|
388
|
+
try {
|
|
389
|
+
await this.exec(["-c", "core.editor=true", "rebase", "--continue"]);
|
|
390
|
+
return { done: true, conflictFiles: [] };
|
|
391
|
+
} catch (err) {
|
|
392
|
+
const msg = err.message || "";
|
|
393
|
+
if (msg.includes("CONFLICT") || msg.includes("could not apply")) {
|
|
394
|
+
const conflictFiles = await this.getConflictFiles();
|
|
395
|
+
return { done: false, conflictFiles };
|
|
768
396
|
}
|
|
769
|
-
|
|
770
|
-
}
|
|
771
|
-
};
|
|
772
|
-
|
|
773
|
-
// src/ai-runner/CodebuddyRunner.ts
|
|
774
|
-
var CodebuddyRunner = class extends BaseAIRunner {
|
|
775
|
-
binary;
|
|
776
|
-
nvmNodeVersion;
|
|
777
|
-
model;
|
|
778
|
-
constructor(binary, nvmNodeVersion, model) {
|
|
779
|
-
super();
|
|
780
|
-
this.binary = binary;
|
|
781
|
-
this.nvmNodeVersion = nvmNodeVersion;
|
|
782
|
-
this.model = model;
|
|
783
|
-
}
|
|
784
|
-
getBinary() {
|
|
785
|
-
return this.binary;
|
|
786
|
-
}
|
|
787
|
-
buildArgs(options) {
|
|
788
|
-
const args = ["-p", "--output-format", "stream-json", "--verbose"];
|
|
789
|
-
if (options.mode === "plan") {
|
|
790
|
-
args.push("--permission-mode", "plan", "--allowedTools", "Read,Grep,Glob,WebSearch");
|
|
791
|
-
} else {
|
|
792
|
-
args.push("-y");
|
|
793
|
-
}
|
|
794
|
-
if (this.model) {
|
|
795
|
-
args.push("--model", this.model);
|
|
796
|
-
}
|
|
797
|
-
if (options.continueSession && options.sessionId) {
|
|
798
|
-
args.push("--resume", options.sessionId);
|
|
397
|
+
throw err;
|
|
799
398
|
}
|
|
800
|
-
return args;
|
|
801
399
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
env: {
|
|
806
|
-
...process.env,
|
|
807
|
-
NODE_VERSION: this.nvmNodeVersion
|
|
808
|
-
}
|
|
809
|
-
};
|
|
400
|
+
async rebaseAbort() {
|
|
401
|
+
await this.exec(["rebase", "--abort"]);
|
|
402
|
+
logger3.info("Rebase aborted");
|
|
810
403
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
var CursorAgentRunner = class extends BaseAIRunner {
|
|
815
|
-
binary;
|
|
816
|
-
nvmNodeVersion;
|
|
817
|
-
model;
|
|
818
|
-
constructor(binary, nvmNodeVersion, model) {
|
|
819
|
-
super();
|
|
820
|
-
this.binary = binary;
|
|
821
|
-
this.nvmNodeVersion = nvmNodeVersion;
|
|
822
|
-
this.model = model;
|
|
823
|
-
}
|
|
824
|
-
getBinary() {
|
|
825
|
-
return this.binary;
|
|
826
|
-
}
|
|
827
|
-
buildArgs(options) {
|
|
828
|
-
const args = [
|
|
829
|
-
"agent",
|
|
830
|
-
"-p",
|
|
831
|
-
"--force",
|
|
832
|
-
"--trust",
|
|
833
|
-
"--output-format",
|
|
834
|
-
"stream-json",
|
|
835
|
-
"--workspace",
|
|
836
|
-
options.workDir
|
|
837
|
-
];
|
|
838
|
-
if (options.mode === "plan") {
|
|
839
|
-
args.push("--mode", "plan");
|
|
840
|
-
}
|
|
841
|
-
if (this.model) {
|
|
842
|
-
args.push("--model", this.model);
|
|
843
|
-
}
|
|
844
|
-
if (options.continueSession && options.sessionId) {
|
|
845
|
-
args.push("--resume", options.sessionId);
|
|
846
|
-
}
|
|
847
|
-
return args;
|
|
404
|
+
async isRebaseInProgress() {
|
|
405
|
+
const status = await this.exec(["status"]);
|
|
406
|
+
return status.includes("rebase in progress");
|
|
848
407
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
env: {
|
|
853
|
-
...process.env,
|
|
854
|
-
NODE_VERSION: this.nvmNodeVersion
|
|
855
|
-
}
|
|
856
|
-
};
|
|
408
|
+
async forcePush(branch) {
|
|
409
|
+
await this.exec(["push", "--no-verify", "--force-with-lease", "-u", "origin", branch]);
|
|
410
|
+
logger3.info("Force pushed", { branch });
|
|
857
411
|
}
|
|
858
412
|
};
|
|
859
413
|
|
|
860
|
-
// src/ai-runner/index.ts
|
|
861
|
-
function createAIRunner(ai) {
|
|
862
|
-
switch (ai.mode) {
|
|
863
|
-
case "cursor-agent":
|
|
864
|
-
return new CursorAgentRunner(ai.binary, ai.nvmNodeVersion, ai.model);
|
|
865
|
-
case "codebuddy":
|
|
866
|
-
return new CodebuddyRunner(ai.binary, ai.nvmNodeVersion, ai.model);
|
|
867
|
-
default:
|
|
868
|
-
return new ClaudeInternalRunner(ai.binary, ai.nvmNodeVersion, ai.model);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
|
|
872
414
|
// src/pipeline/PipelineDefinition.ts
|
|
873
415
|
var CLASSIC_PIPELINE = {
|
|
874
416
|
mode: "classic",
|
|
@@ -998,35 +540,35 @@ var EventBusImpl = class extends EventEmitter {
|
|
|
998
540
|
var eventBus = new EventBusImpl();
|
|
999
541
|
|
|
1000
542
|
// src/tracker/IssueTracker.ts
|
|
1001
|
-
import
|
|
1002
|
-
import
|
|
1003
|
-
var
|
|
543
|
+
import fs2 from "fs";
|
|
544
|
+
import path2 from "path";
|
|
545
|
+
var logger4 = logger.child("IssueTracker");
|
|
1004
546
|
var IssueTracker = class _IssueTracker {
|
|
1005
547
|
filePath;
|
|
1006
548
|
data;
|
|
1007
549
|
constructor(dataDir) {
|
|
1008
|
-
this.filePath =
|
|
550
|
+
this.filePath = path2.join(dataDir, "tracker.json");
|
|
1009
551
|
this.data = this.load();
|
|
1010
552
|
}
|
|
1011
553
|
load() {
|
|
1012
554
|
try {
|
|
1013
|
-
if (
|
|
1014
|
-
const raw =
|
|
555
|
+
if (fs2.existsSync(this.filePath)) {
|
|
556
|
+
const raw = fs2.readFileSync(this.filePath, "utf-8");
|
|
1015
557
|
return JSON.parse(raw);
|
|
1016
558
|
}
|
|
1017
559
|
} catch (err) {
|
|
1018
|
-
|
|
560
|
+
logger4.error("Failed to load tracker data", { error: err.message });
|
|
1019
561
|
}
|
|
1020
562
|
return { issues: {} };
|
|
1021
563
|
}
|
|
1022
564
|
save() {
|
|
1023
|
-
const dir =
|
|
1024
|
-
if (!
|
|
1025
|
-
|
|
565
|
+
const dir = path2.dirname(this.filePath);
|
|
566
|
+
if (!fs2.existsSync(dir)) {
|
|
567
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
1026
568
|
}
|
|
1027
|
-
const tmpPath =
|
|
1028
|
-
|
|
1029
|
-
|
|
569
|
+
const tmpPath = path2.join(dir, `.tracker-${process.pid}-${Date.now()}.tmp`);
|
|
570
|
+
fs2.writeFileSync(tmpPath, JSON.stringify(this.data, null, 2), "utf-8");
|
|
571
|
+
fs2.renameSync(tmpPath, this.filePath);
|
|
1030
572
|
}
|
|
1031
573
|
key(issueIid) {
|
|
1032
574
|
return String(issueIid);
|
|
@@ -1044,7 +586,7 @@ var IssueTracker = class _IssueTracker {
|
|
|
1044
586
|
};
|
|
1045
587
|
this.data.issues[this.key(record.issueIid)] = full;
|
|
1046
588
|
this.save();
|
|
1047
|
-
|
|
589
|
+
logger4.info("Issue tracked", { issueIid: record.issueIid, state: record.state });
|
|
1048
590
|
eventBus.emitTyped("issue:created", full);
|
|
1049
591
|
return full;
|
|
1050
592
|
}
|
|
@@ -1063,7 +605,7 @@ var IssueTracker = class _IssueTracker {
|
|
|
1063
605
|
Object.assign(record, extra);
|
|
1064
606
|
}
|
|
1065
607
|
this.save();
|
|
1066
|
-
|
|
608
|
+
logger4.info("Issue state updated", { issueIid, state });
|
|
1067
609
|
eventBus.emitTyped("issue:stateChanged", { issueIid, state, record });
|
|
1068
610
|
}
|
|
1069
611
|
markFailed(issueIid, error, failedAtState) {
|
|
@@ -1075,7 +617,7 @@ var IssueTracker = class _IssueTracker {
|
|
|
1075
617
|
record.attempts += 1;
|
|
1076
618
|
record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1077
619
|
this.save();
|
|
1078
|
-
|
|
620
|
+
logger4.warn("Issue marked as failed", { issueIid, error, failedAtState, attempts: record.attempts });
|
|
1079
621
|
eventBus.emitTyped("issue:failed", { issueIid, error, failedAtState, record });
|
|
1080
622
|
}
|
|
1081
623
|
static TERMINAL_STATES = /* @__PURE__ */ new Set(["completed" /* Completed */, "failed" /* Failed */]);
|
|
@@ -1142,7 +684,7 @@ var IssueTracker = class _IssueTracker {
|
|
|
1142
684
|
record.lastError = void 0;
|
|
1143
685
|
record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1144
686
|
this.save();
|
|
1145
|
-
|
|
687
|
+
logger4.info("Issue fully reset", { issueIid });
|
|
1146
688
|
eventBus.emitTyped("issue:restarted", { issueIid, record });
|
|
1147
689
|
return true;
|
|
1148
690
|
}
|
|
@@ -1157,7 +699,7 @@ var IssueTracker = class _IssueTracker {
|
|
|
1157
699
|
record.lastError = void 0;
|
|
1158
700
|
record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1159
701
|
this.save();
|
|
1160
|
-
|
|
702
|
+
logger4.info("Issue reset to phase", { issueIid, phase, state: targetState });
|
|
1161
703
|
eventBus.emitTyped("issue:retryFromPhase", { issueIid, phase, record });
|
|
1162
704
|
return true;
|
|
1163
705
|
}
|
|
@@ -1169,7 +711,7 @@ var IssueTracker = class _IssueTracker {
|
|
|
1169
711
|
record.lastError = void 0;
|
|
1170
712
|
record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1171
713
|
this.save();
|
|
1172
|
-
|
|
714
|
+
logger4.info("Issue reset for retry", { issueIid, restoreState });
|
|
1173
715
|
eventBus.emitTyped("issue:resetForRetry", { issueIid, restoreState, record });
|
|
1174
716
|
return true;
|
|
1175
717
|
}
|
|
@@ -1179,16 +721,53 @@ var IssueTracker = class _IssueTracker {
|
|
|
1179
721
|
const record = this.data.issues[key];
|
|
1180
722
|
delete this.data.issues[key];
|
|
1181
723
|
this.save();
|
|
1182
|
-
|
|
724
|
+
logger4.info("Issue deleted from tracker", { issueIid });
|
|
1183
725
|
eventBus.emitTyped("issue:deleted", { issueIid, record });
|
|
1184
726
|
return true;
|
|
1185
727
|
}
|
|
728
|
+
static IN_PROGRESS_STATES = /* @__PURE__ */ new Set([
|
|
729
|
+
"analyzing" /* Analyzing */,
|
|
730
|
+
"designing" /* Designing */,
|
|
731
|
+
"implementing" /* Implementing */,
|
|
732
|
+
"verifying" /* Verifying */,
|
|
733
|
+
"planning" /* Planning */,
|
|
734
|
+
"building" /* Building */,
|
|
735
|
+
"resolving_conflict" /* ResolvingConflict */
|
|
736
|
+
]);
|
|
737
|
+
recoverInterruptedIssues() {
|
|
738
|
+
let count = 0;
|
|
739
|
+
for (const record of Object.values(this.data.issues)) {
|
|
740
|
+
if (_IssueTracker.IN_PROGRESS_STATES.has(record.state)) {
|
|
741
|
+
logger4.warn("Recovering interrupted issue", {
|
|
742
|
+
issueIid: record.issueIid,
|
|
743
|
+
state: record.state
|
|
744
|
+
});
|
|
745
|
+
record.failedAtState = record.state;
|
|
746
|
+
record.state = "failed" /* Failed */;
|
|
747
|
+
record.lastError = "Interrupted by service restart";
|
|
748
|
+
record.attempts += 1;
|
|
749
|
+
record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
750
|
+
count++;
|
|
751
|
+
eventBus.emitTyped("issue:failed", {
|
|
752
|
+
issueIid: record.issueIid,
|
|
753
|
+
error: "Interrupted by service restart",
|
|
754
|
+
failedAtState: record.failedAtState,
|
|
755
|
+
record
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (count > 0) {
|
|
760
|
+
this.save();
|
|
761
|
+
logger4.info("Recovered interrupted issues", { count });
|
|
762
|
+
}
|
|
763
|
+
return count;
|
|
764
|
+
}
|
|
1186
765
|
};
|
|
1187
766
|
|
|
1188
767
|
// src/persistence/PlanPersistence.ts
|
|
1189
|
-
import
|
|
1190
|
-
import
|
|
1191
|
-
var
|
|
768
|
+
import fs3 from "fs";
|
|
769
|
+
import path3 from "path";
|
|
770
|
+
var logger5 = logger.child("PlanPersistence");
|
|
1192
771
|
var PLAN_DIR = ".claude-plan";
|
|
1193
772
|
var PlanPersistence = class _PlanPersistence {
|
|
1194
773
|
workDir;
|
|
@@ -1201,57 +780,57 @@ var PlanPersistence = class _PlanPersistence {
|
|
|
1201
780
|
return this.workDir;
|
|
1202
781
|
}
|
|
1203
782
|
get planDir() {
|
|
1204
|
-
return
|
|
783
|
+
return path3.join(this.workDir, PLAN_DIR, `issue-${this.issueIid}`);
|
|
1205
784
|
}
|
|
1206
785
|
ensureDir() {
|
|
1207
|
-
if (!
|
|
1208
|
-
|
|
786
|
+
if (!fs3.existsSync(this.planDir)) {
|
|
787
|
+
fs3.mkdirSync(this.planDir, { recursive: true });
|
|
1209
788
|
}
|
|
1210
789
|
}
|
|
1211
790
|
writeIssueMeta(meta) {
|
|
1212
791
|
this.ensureDir();
|
|
1213
|
-
const filePath =
|
|
1214
|
-
|
|
1215
|
-
|
|
792
|
+
const filePath = path3.join(this.planDir, "issue-meta.json");
|
|
793
|
+
fs3.writeFileSync(filePath, JSON.stringify(meta, null, 2), "utf-8");
|
|
794
|
+
logger5.info("Issue meta written");
|
|
1216
795
|
}
|
|
1217
796
|
writeProgress(data) {
|
|
1218
797
|
this.ensureDir();
|
|
1219
|
-
const filePath =
|
|
1220
|
-
|
|
1221
|
-
|
|
798
|
+
const filePath = path3.join(this.planDir, "progress.json");
|
|
799
|
+
fs3.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
800
|
+
logger5.debug("Progress written", { currentPhase: data.currentPhase });
|
|
1222
801
|
}
|
|
1223
802
|
readProgress() {
|
|
1224
|
-
const filePath =
|
|
1225
|
-
if (!
|
|
803
|
+
const filePath = path3.join(this.planDir, "progress.json");
|
|
804
|
+
if (!fs3.existsSync(filePath)) return null;
|
|
1226
805
|
try {
|
|
1227
|
-
return JSON.parse(
|
|
806
|
+
return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
1228
807
|
} catch {
|
|
1229
808
|
return null;
|
|
1230
809
|
}
|
|
1231
810
|
}
|
|
1232
811
|
writeAnalysis(content) {
|
|
1233
812
|
this.ensureDir();
|
|
1234
|
-
|
|
1235
|
-
|
|
813
|
+
fs3.writeFileSync(path3.join(this.planDir, "01-analysis.md"), content, "utf-8");
|
|
814
|
+
logger5.info("Analysis document written");
|
|
1236
815
|
}
|
|
1237
816
|
writeDesign(content) {
|
|
1238
817
|
this.ensureDir();
|
|
1239
|
-
|
|
1240
|
-
|
|
818
|
+
fs3.writeFileSync(path3.join(this.planDir, "02-design.md"), content, "utf-8");
|
|
819
|
+
logger5.info("Design document written");
|
|
1241
820
|
}
|
|
1242
821
|
writeTodolist(content) {
|
|
1243
822
|
this.ensureDir();
|
|
1244
|
-
|
|
1245
|
-
|
|
823
|
+
fs3.writeFileSync(path3.join(this.planDir, "03-todolist.md"), content, "utf-8");
|
|
824
|
+
logger5.info("Todolist written");
|
|
1246
825
|
}
|
|
1247
826
|
writeVerifyReport(content, filename = "04-verify-report.md") {
|
|
1248
827
|
this.ensureDir();
|
|
1249
|
-
|
|
1250
|
-
|
|
828
|
+
fs3.writeFileSync(path3.join(this.planDir, filename), content, "utf-8");
|
|
829
|
+
logger5.info("Verify report written", { filename });
|
|
1251
830
|
}
|
|
1252
831
|
getAllPlanFiles() {
|
|
1253
|
-
if (!
|
|
1254
|
-
return
|
|
832
|
+
if (!fs3.existsSync(this.planDir)) return [];
|
|
833
|
+
return fs3.readdirSync(this.planDir).map((f) => path3.join(PLAN_DIR, `issue-${this.issueIid}`, f));
|
|
1255
834
|
}
|
|
1256
835
|
createInitialProgress(issueId, issueTitle, branchName, def) {
|
|
1257
836
|
const pending = { status: "pending" };
|
|
@@ -1284,8 +863,8 @@ var PlanPersistence = class _PlanPersistence {
|
|
|
1284
863
|
}
|
|
1285
864
|
writePlan(content) {
|
|
1286
865
|
this.ensureDir();
|
|
1287
|
-
|
|
1288
|
-
|
|
866
|
+
fs3.writeFileSync(path3.join(this.planDir, "01-plan.md"), content, "utf-8");
|
|
867
|
+
logger5.info("Plan document written");
|
|
1289
868
|
}
|
|
1290
869
|
writeReviewFeedback(content) {
|
|
1291
870
|
this.ensureDir();
|
|
@@ -1296,32 +875,32 @@ var PlanPersistence = class _PlanPersistence {
|
|
|
1296
875
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1297
876
|
};
|
|
1298
877
|
history.push(round);
|
|
1299
|
-
|
|
1300
|
-
|
|
878
|
+
fs3.writeFileSync(
|
|
879
|
+
path3.join(this.planDir, "review-history.json"),
|
|
1301
880
|
JSON.stringify(history, null, 2),
|
|
1302
881
|
"utf-8"
|
|
1303
882
|
);
|
|
1304
|
-
|
|
1305
|
-
|
|
883
|
+
fs3.writeFileSync(
|
|
884
|
+
path3.join(this.planDir, "review-feedback.md"),
|
|
1306
885
|
_PlanPersistence.renderReviewHistoryMarkdown(history),
|
|
1307
886
|
"utf-8"
|
|
1308
887
|
);
|
|
1309
|
-
|
|
888
|
+
logger5.info("Review feedback appended", { round: round.round });
|
|
1310
889
|
}
|
|
1311
890
|
readReviewFeedback() {
|
|
1312
|
-
const filePath =
|
|
1313
|
-
if (!
|
|
891
|
+
const filePath = path3.join(this.planDir, "review-feedback.md");
|
|
892
|
+
if (!fs3.existsSync(filePath)) return null;
|
|
1314
893
|
try {
|
|
1315
|
-
return
|
|
894
|
+
return fs3.readFileSync(filePath, "utf-8");
|
|
1316
895
|
} catch {
|
|
1317
896
|
return null;
|
|
1318
897
|
}
|
|
1319
898
|
}
|
|
1320
899
|
readReviewHistory() {
|
|
1321
|
-
const filePath =
|
|
1322
|
-
if (!
|
|
900
|
+
const filePath = path3.join(this.planDir, "review-history.json");
|
|
901
|
+
if (!fs3.existsSync(filePath)) return [];
|
|
1323
902
|
try {
|
|
1324
|
-
const data = JSON.parse(
|
|
903
|
+
const data = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
1325
904
|
return Array.isArray(data) ? data : [];
|
|
1326
905
|
} catch {
|
|
1327
906
|
return [];
|
|
@@ -1361,13 +940,43 @@ var PlanPersistence = class _PlanPersistence {
|
|
|
1361
940
|
};
|
|
1362
941
|
|
|
1363
942
|
// src/phases/BasePhase.ts
|
|
1364
|
-
import
|
|
1365
|
-
import
|
|
943
|
+
import fs4 from "fs";
|
|
944
|
+
import path5 from "path";
|
|
1366
945
|
|
|
1367
946
|
// src/prompts/templates.ts
|
|
1368
947
|
function planDir(iid) {
|
|
1369
948
|
return `.claude-plan/issue-${iid}`;
|
|
1370
949
|
}
|
|
950
|
+
function getKnowledgeForPrompt() {
|
|
951
|
+
const k = getProjectKnowledge() ?? KNOWLEDGE_DEFAULTS;
|
|
952
|
+
const codeStyleParts = [];
|
|
953
|
+
if (k.codeStyle.indentStyle === "spaces") {
|
|
954
|
+
codeStyleParts.push(`${k.codeStyle.indentSize}\u7A7A\u683C\u7F29\u8FDB`);
|
|
955
|
+
} else {
|
|
956
|
+
codeStyleParts.push("Tab\u7F29\u8FDB");
|
|
957
|
+
}
|
|
958
|
+
codeStyleParts.push(`${k.codeStyle.lineWidth}\u5B57\u7B26\u884C\u5BBD`);
|
|
959
|
+
codeStyleParts.push("\u547D\u540D\u89C4\u8303\u7B49");
|
|
960
|
+
if (k.codeStyle.additionalRules?.length) {
|
|
961
|
+
codeStyleParts.push(...k.codeStyle.additionalRules);
|
|
962
|
+
}
|
|
963
|
+
const knownIssueLines = k.knownIssues.map((issue) => `- ${issue.description}${issue.advice ? `\uFF0C${issue.advice}` : ""}`);
|
|
964
|
+
return {
|
|
965
|
+
dependencyCheckPath: k.toolchain.dependencyCheckPath ?? "node_modules/.bin/eslint",
|
|
966
|
+
installCommand: k.toolchain.installCommand,
|
|
967
|
+
installFallbackCommand: k.toolchain.installFallbackCommand ?? `${k.toolchain.installCommand} --ignore-scripts`,
|
|
968
|
+
lintCommand: k.toolchain.lintCommand ?? "npm run lint",
|
|
969
|
+
buildCommand: k.toolchain.buildCommand ?? "npm run build",
|
|
970
|
+
testCommand: k.toolchain.testCommand ?? "npm test",
|
|
971
|
+
testFilesCommand: k.toolchain.testFilesCommand ?? `${k.toolchain.testCommand ?? "npm test"} -- <\u6D89\u53CA\u53D8\u66F4\u7684\u6D4B\u8BD5\u6587\u4EF6>`,
|
|
972
|
+
knownIssuesSection: knownIssueLines.length > 0 ? knownIssueLines.join("\n") : "- \u65E0\u5DF2\u77E5\u9884\u5B58\u95EE\u9898",
|
|
973
|
+
codeStyleDescription: codeStyleParts.join("\u3001"),
|
|
974
|
+
// E2E related
|
|
975
|
+
e2eDir: k.structure.e2eDir ?? "e2e",
|
|
976
|
+
e2eTool: k.structure.e2eTool ?? "E2E",
|
|
977
|
+
frontendDir: k.structure.frontendDir ?? "frontend"
|
|
978
|
+
};
|
|
979
|
+
}
|
|
1371
980
|
function analysisPrompt(ctx) {
|
|
1372
981
|
const supplementSection = ctx.supplementText ? `
|
|
1373
982
|
|
|
@@ -1395,26 +1004,32 @@ ${ctx.supplementText}` : "";
|
|
|
1395
1004
|
}
|
|
1396
1005
|
function implementPrompt(ctx) {
|
|
1397
1006
|
const pd = planDir(ctx.issueIid);
|
|
1007
|
+
const kv = getKnowledgeForPrompt();
|
|
1398
1008
|
return t("prompt.implement", {
|
|
1399
1009
|
iid: ctx.issueIid,
|
|
1400
1010
|
title: ctx.issueTitle,
|
|
1401
|
-
planDir: pd
|
|
1011
|
+
planDir: pd,
|
|
1012
|
+
...kv
|
|
1402
1013
|
});
|
|
1403
1014
|
}
|
|
1404
1015
|
function verifyPrompt(ctx) {
|
|
1405
1016
|
const pd = planDir(ctx.issueIid);
|
|
1017
|
+
const kv = getKnowledgeForPrompt();
|
|
1406
1018
|
return t("prompt.verify", {
|
|
1407
1019
|
iid: ctx.issueIid,
|
|
1408
1020
|
title: ctx.issueTitle,
|
|
1409
|
-
planDir: pd
|
|
1021
|
+
planDir: pd,
|
|
1022
|
+
...kv
|
|
1410
1023
|
});
|
|
1411
1024
|
}
|
|
1412
1025
|
function planModeVerifyPrompt(ctx) {
|
|
1413
1026
|
const pd = planDir(ctx.issueIid);
|
|
1027
|
+
const kv = getKnowledgeForPrompt();
|
|
1414
1028
|
return t("prompt.planModeVerify", {
|
|
1415
1029
|
iid: ctx.issueIid,
|
|
1416
1030
|
title: ctx.issueTitle,
|
|
1417
|
-
planDir: pd
|
|
1031
|
+
planDir: pd,
|
|
1032
|
+
...kv
|
|
1418
1033
|
});
|
|
1419
1034
|
}
|
|
1420
1035
|
function planPrompt(ctx) {
|
|
@@ -1432,10 +1047,12 @@ ${ctx.supplementText}` : "";
|
|
|
1432
1047
|
}
|
|
1433
1048
|
function buildPrompt(ctx) {
|
|
1434
1049
|
const pd = planDir(ctx.issueIid);
|
|
1050
|
+
const kv = getKnowledgeForPrompt();
|
|
1435
1051
|
return t("prompt.build", {
|
|
1436
1052
|
iid: ctx.issueIid,
|
|
1437
1053
|
title: ctx.issueTitle,
|
|
1438
|
-
planDir: pd
|
|
1054
|
+
planDir: pd,
|
|
1055
|
+
...kv
|
|
1439
1056
|
});
|
|
1440
1057
|
}
|
|
1441
1058
|
function rePlanPrompt(ctx, history) {
|
|
@@ -1457,6 +1074,10 @@ ${ctx.supplementText}` : "";
|
|
|
1457
1074
|
});
|
|
1458
1075
|
}
|
|
1459
1076
|
function e2eVerifyPromptSuffix(ctx, ports) {
|
|
1077
|
+
const kv = getKnowledgeForPrompt();
|
|
1078
|
+
const frontendDir = kv.frontendDir;
|
|
1079
|
+
const e2eDir = kv.e2eDir;
|
|
1080
|
+
const e2eTool = kv.e2eTool;
|
|
1460
1081
|
const serverSection = ports ? `
|
|
1461
1082
|
**Preview \u73AF\u5883\u5DF2\u542F\u52A8\uFF08\u7531\u7CFB\u7EDF\u7BA1\u7406\uFF0C\u65E0\u9700\u624B\u52A8\u542F\u52A8\uFF09\uFF1A**
|
|
1462
1083
|
- \u540E\u7AEF: http://${ports.host}:${ports.backendPort}
|
|
@@ -1465,13 +1086,13 @@ function e2eVerifyPromptSuffix(ctx, ports) {
|
|
|
1465
1086
|
\u6267\u884C E2E \u6D4B\u8BD5\u65F6\u8BF7\u4F7F\u7528\u4EE5\u4E0B\u73AF\u5883\u53D8\u91CF\u6765\u8FDE\u63A5\u5DF2\u542F\u52A8\u7684\u670D\u52A1\uFF1A
|
|
1466
1087
|
\`\`\`bash
|
|
1467
1088
|
E2E_PORT=${ports.frontendPort} E2E_HOST=${ports.host} E2E_BASE_URL=https://${ports.host}:${ports.frontendPort} \\
|
|
1468
|
-
cd
|
|
1089
|
+
cd ${frontendDir} && npx ${e2eTool.toLowerCase()} test
|
|
1469
1090
|
\`\`\`
|
|
1470
1091
|
|
|
1471
|
-
**\u6CE8\u610F**: \u4E0D\u8981\u4F7F\u7528 pnpm test:e2e\uFF08\u5B83\u4F1A\u5C1D\u8BD5\u81EA\u884C\u542F\u52A8 webServer\uFF09\uFF0C\u76F4\u63A5\u7528 npx
|
|
1092
|
+
**\u6CE8\u610F**: \u4E0D\u8981\u4F7F\u7528 pnpm test:e2e\uFF08\u5B83\u4F1A\u5C1D\u8BD5\u81EA\u884C\u542F\u52A8 webServer\uFF09\uFF0C\u76F4\u63A5\u7528 npx ${e2eTool.toLowerCase()} test \u5373\u53EF\u590D\u7528\u5DF2\u542F\u52A8\u7684\u524D\u7AEF\u3002` : `
|
|
1472
1093
|
\u6267\u884C E2E \u6D4B\u8BD5\uFF1A
|
|
1473
1094
|
\`\`\`bash
|
|
1474
|
-
cd
|
|
1095
|
+
cd ${frontendDir} && pnpm test:e2e
|
|
1475
1096
|
\`\`\``;
|
|
1476
1097
|
return `
|
|
1477
1098
|
|
|
@@ -1479,8 +1100,8 @@ cd frontend && pnpm test:e2e
|
|
|
1479
1100
|
|
|
1480
1101
|
\u672C\u6B21\u53D8\u66F4\u5DF2\u5F00\u542F E2E UI \u81EA\u52A8\u9A8C\u6536\uFF0C\u8BF7\u989D\u5916\u6267\u884C\u4EE5\u4E0B\u6B65\u9AA4\uFF1A
|
|
1481
1102
|
|
|
1482
|
-
6. \u5982\u679C\u672C\u6B21\u53D8\u66F4\u6D89\u53CA\u524D\u7AEF\u9875\u9762\
|
|
1483
|
-
a. \u5728
|
|
1103
|
+
6. \u5982\u679C\u672C\u6B21\u53D8\u66F4\u6D89\u53CA\u524D\u7AEF\u9875\u9762\uFF08${frontendDir}/ \u76EE\u5F55\u6709\u6539\u52A8\uFF09\uFF0C\u8BF7\u6267\u884C UI E2E \u9A8C\u8BC1\uFF1A
|
|
1104
|
+
a. \u5728 ${e2eDir}/ \u76EE\u5F55\u4E0B\u7F16\u5199\u9488\u5BF9\u672C\u6B21\u53D8\u66F4\u7684 ${e2eTool} \u6D4B\u8BD5
|
|
1484
1105
|
b. ${serverSection.trim()}
|
|
1485
1106
|
c. \u5982\u679C\u6D4B\u8BD5\u5931\u8D25\uFF0C\u5206\u6790\u5931\u8D25\u539F\u56E0\u5E76\u5C1D\u8BD5\u4FEE\u590D
|
|
1486
1107
|
7. \u5C06 E2E \u6D4B\u8BD5\u7ED3\u679C\u5199\u5165\u9A8C\u8BC1\u62A5\u544A\u7684 **E2E UI \u6D4B\u8BD5\u7ED3\u679C** \u7AE0\u8282\uFF0C\u5305\u62EC\uFF1A
|
|
@@ -1488,6 +1109,15 @@ cd frontend && pnpm test:e2e
|
|
|
1488
1109
|
- \u4E13\u9879\u6D4B\u8BD5\u7ED3\u679C\u5217\u8868
|
|
1489
1110
|
- \u5931\u8D25\u622A\u56FE\u8DEF\u5F84\uFF08\u5982\u6709\uFF09`;
|
|
1490
1111
|
}
|
|
1112
|
+
function conflictResolvePrompt(ctx) {
|
|
1113
|
+
const conflictFilesList = ctx.conflictFiles.map((f) => `- \`${f}\``).join("\n");
|
|
1114
|
+
return t("prompt.conflictResolve", {
|
|
1115
|
+
iid: ctx.issueIid,
|
|
1116
|
+
branch: ctx.branchName,
|
|
1117
|
+
baseBranch: ctx.baseBranch,
|
|
1118
|
+
conflictFilesList
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1491
1121
|
function issueProgressComment(phase, status, detail) {
|
|
1492
1122
|
const emoji = {
|
|
1493
1123
|
analysis: "\u{1F50D}",
|
|
@@ -1512,42 +1142,12 @@ ${detail}`;
|
|
|
1512
1142
|
|
|
1513
1143
|
// src/rules/RuleResolver.ts
|
|
1514
1144
|
import { readdir, readFile } from "fs/promises";
|
|
1515
|
-
import
|
|
1516
|
-
var RULE_TRIGGERS = [
|
|
1517
|
-
{
|
|
1518
|
-
filename: "session-rule.mdc",
|
|
1519
|
-
keywords: ["Session", "PublishSession", "SessionData", "\u4F1A\u8BDD", "ActivitySession"]
|
|
1520
|
-
},
|
|
1521
|
-
{
|
|
1522
|
-
filename: "backend-api-implementation.mdc",
|
|
1523
|
-
keywords: ["\u65B0\u589E\u63A5\u53E3", "\u6DFB\u52A0\u63A5\u53E3", "\u4FEE\u6539\u63A5\u53E3", "Controller", "ServiceImpl", "\u8DEF\u7531"]
|
|
1524
|
-
},
|
|
1525
|
-
{
|
|
1526
|
-
filename: "artifact-publish-rule.mdc",
|
|
1527
|
-
keywords: ["\u5236\u54C1\u53D1\u5E03", "ArtifactPublish", "lockResource", "unlockResource", "\u53D1\u5E03\u7CFB\u7EDF"]
|
|
1528
|
-
},
|
|
1529
|
-
{
|
|
1530
|
-
filename: "activity-realization-rule.mdc",
|
|
1531
|
-
keywords: ["\u6D3B\u52A8\u5B9E\u73B0", "ActivityRealization", "BaseActivityRealization", "syncExecuteStatus", "queryExec"]
|
|
1532
|
-
},
|
|
1533
|
-
{
|
|
1534
|
-
filename: "appset.mdc",
|
|
1535
|
-
keywords: ["AppSet", "appset", "ComponentInstance", "StorageService", "IDC Set"]
|
|
1536
|
-
},
|
|
1537
|
-
{
|
|
1538
|
-
filename: "add-artifact-workflow.mdc",
|
|
1539
|
-
keywords: ["\u6DFB\u52A0\u5236\u54C1", "\u65B0\u589E\u5236\u54C1", "\u5236\u54C1\u7C7B\u578B", "ArtifactType", "ArtifactTypeId"]
|
|
1540
|
-
},
|
|
1541
|
-
{
|
|
1542
|
-
filename: "add-appset-api-workflow.mdc",
|
|
1543
|
-
keywords: ["AppSet\u63A5\u53E3", "AppSet API", "proto", "protobuf", "devops-contracts"]
|
|
1544
|
-
}
|
|
1545
|
-
];
|
|
1145
|
+
import path4 from "path";
|
|
1546
1146
|
function parseFrontmatter(raw) {
|
|
1547
1147
|
const fmRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
|
1548
1148
|
const match = raw.match(fmRegex);
|
|
1549
1149
|
if (!match) {
|
|
1550
|
-
return { description: "", content: raw.trim() };
|
|
1150
|
+
return { description: "", alwaysApply: false, content: raw.trim() };
|
|
1551
1151
|
}
|
|
1552
1152
|
const yamlBlock = match[1];
|
|
1553
1153
|
const content = raw.slice(match[0].length).trim();
|
|
@@ -1556,10 +1156,19 @@ function parseFrontmatter(raw) {
|
|
|
1556
1156
|
if (descMatch) {
|
|
1557
1157
|
description = descMatch[1].trim();
|
|
1558
1158
|
}
|
|
1559
|
-
|
|
1159
|
+
let alwaysApply = false;
|
|
1160
|
+
const applyMatch = yamlBlock.match(/alwaysApply:\s*(true|false)/i);
|
|
1161
|
+
if (applyMatch) {
|
|
1162
|
+
alwaysApply = applyMatch[1].toLowerCase() === "true";
|
|
1163
|
+
}
|
|
1164
|
+
return { description, alwaysApply, content };
|
|
1560
1165
|
}
|
|
1561
1166
|
var RuleResolver = class {
|
|
1562
1167
|
rules = [];
|
|
1168
|
+
triggers;
|
|
1169
|
+
constructor(ruleTriggers) {
|
|
1170
|
+
this.triggers = ruleTriggers;
|
|
1171
|
+
}
|
|
1563
1172
|
async loadRules(rulesDir) {
|
|
1564
1173
|
this.rules = [];
|
|
1565
1174
|
let files;
|
|
@@ -1571,10 +1180,10 @@ var RuleResolver = class {
|
|
|
1571
1180
|
const mdcFiles = files.filter((f) => f.endsWith(".mdc"));
|
|
1572
1181
|
const loadPromises = mdcFiles.map(async (filename) => {
|
|
1573
1182
|
try {
|
|
1574
|
-
const raw = await readFile(
|
|
1575
|
-
const { description, content } = parseFrontmatter(raw);
|
|
1183
|
+
const raw = await readFile(path4.join(rulesDir, filename), "utf-8");
|
|
1184
|
+
const { description, alwaysApply, content } = parseFrontmatter(raw);
|
|
1576
1185
|
if (content) {
|
|
1577
|
-
this.rules.push({ filename, description, content });
|
|
1186
|
+
this.rules.push({ filename, description, alwaysApply, content });
|
|
1578
1187
|
}
|
|
1579
1188
|
} catch {
|
|
1580
1189
|
}
|
|
@@ -1586,16 +1195,34 @@ var RuleResolver = class {
|
|
|
1586
1195
|
}
|
|
1587
1196
|
matchRules(text) {
|
|
1588
1197
|
const lowerText = text.toLowerCase();
|
|
1589
|
-
const matched =
|
|
1590
|
-
for (const
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
const rule = this.rules.find((r) => r.filename === trigger.filename);
|
|
1594
|
-
if (rule) {
|
|
1595
|
-
matched.push(rule);
|
|
1198
|
+
const matched = /* @__PURE__ */ new Map();
|
|
1199
|
+
for (const rule of this.rules) {
|
|
1200
|
+
if (rule.alwaysApply) {
|
|
1201
|
+
matched.set(rule.filename, rule);
|
|
1596
1202
|
}
|
|
1597
1203
|
}
|
|
1598
|
-
|
|
1204
|
+
if (this.triggers && this.triggers.length > 0) {
|
|
1205
|
+
for (const trigger of this.triggers) {
|
|
1206
|
+
if (matched.has(trigger.filename)) continue;
|
|
1207
|
+
const isTriggered = trigger.keywords.some((kw) => lowerText.includes(kw.toLowerCase()));
|
|
1208
|
+
if (!isTriggered) continue;
|
|
1209
|
+
const rule = this.rules.find((r) => r.filename === trigger.filename);
|
|
1210
|
+
if (rule) {
|
|
1211
|
+
matched.set(rule.filename, rule);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
} else {
|
|
1215
|
+
for (const rule of this.rules) {
|
|
1216
|
+
if (matched.has(rule.filename)) continue;
|
|
1217
|
+
if (!rule.description) continue;
|
|
1218
|
+
const descWords = rule.description.split(/[\s,,;;、/|]+/).filter((w) => w.length >= 2);
|
|
1219
|
+
const isMatched = descWords.some((word) => lowerText.includes(word.toLowerCase()));
|
|
1220
|
+
if (isMatched) {
|
|
1221
|
+
matched.set(rule.filename, rule);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return Array.from(matched.values());
|
|
1599
1226
|
}
|
|
1600
1227
|
formatForPrompt(rules) {
|
|
1601
1228
|
if (rules.length === 0) return "";
|
|
@@ -1743,8 +1370,9 @@ ${t("basePhase.rulesSection", { rules: matchedRulesText })}` : basePrompt;
|
|
|
1743
1370
|
}
|
|
1744
1371
|
async resolveRules(ctx) {
|
|
1745
1372
|
try {
|
|
1746
|
-
const rulesDir =
|
|
1747
|
-
const
|
|
1373
|
+
const rulesDir = path5.join(this.plan.baseDir, ".cursor", "rules");
|
|
1374
|
+
const knowledge = getProjectKnowledge();
|
|
1375
|
+
const resolver = new RuleResolver(knowledge?.ruleTriggers);
|
|
1748
1376
|
await resolver.loadRules(rulesDir);
|
|
1749
1377
|
const context = `${ctx.issueTitle} ${ctx.issueDescription} ${ctx.supplementText ?? ""}`;
|
|
1750
1378
|
const matched = resolver.matchRules(context);
|
|
@@ -1803,11 +1431,11 @@ ${t("basePhase.rulesSection", { rules: matchedRulesText })}` : basePrompt;
|
|
|
1803
1431
|
}
|
|
1804
1432
|
}
|
|
1805
1433
|
readResultFile(issueIid, filename) {
|
|
1806
|
-
const planDir2 =
|
|
1807
|
-
const filePath =
|
|
1808
|
-
if (!
|
|
1434
|
+
const planDir2 = path5.join(this.plan.baseDir, ".claude-plan", `issue-${issueIid}`);
|
|
1435
|
+
const filePath = path5.join(planDir2, filename);
|
|
1436
|
+
if (!fs4.existsSync(filePath)) return null;
|
|
1809
1437
|
try {
|
|
1810
|
-
return
|
|
1438
|
+
return fs4.readFileSync(filePath, "utf-8");
|
|
1811
1439
|
} catch {
|
|
1812
1440
|
return null;
|
|
1813
1441
|
}
|
|
@@ -1872,7 +1500,7 @@ var ImplementPhase = class extends BasePhase {
|
|
|
1872
1500
|
};
|
|
1873
1501
|
|
|
1874
1502
|
// src/phases/VerifyPhase.ts
|
|
1875
|
-
import
|
|
1503
|
+
import os from "os";
|
|
1876
1504
|
|
|
1877
1505
|
// src/e2e/E2eSettings.ts
|
|
1878
1506
|
var e2eOverride;
|
|
@@ -1890,7 +1518,7 @@ function isE2eEnabledForIssue(issueIid, tracker, cfg) {
|
|
|
1890
1518
|
|
|
1891
1519
|
// src/phases/VerifyPhase.ts
|
|
1892
1520
|
function getDefaultHost() {
|
|
1893
|
-
const interfaces =
|
|
1521
|
+
const interfaces = os.networkInterfaces();
|
|
1894
1522
|
for (const addrs of Object.values(interfaces)) {
|
|
1895
1523
|
for (const addr of addrs ?? []) {
|
|
1896
1524
|
if (addr.family === "IPv4" && !addr.internal) return addr.address;
|
|
@@ -2030,15 +1658,15 @@ var AsyncMutex = class {
|
|
|
2030
1658
|
};
|
|
2031
1659
|
|
|
2032
1660
|
// src/orchestrator/PipelineOrchestrator.ts
|
|
2033
|
-
import
|
|
2034
|
-
import
|
|
2035
|
-
import
|
|
1661
|
+
import path9 from "path";
|
|
1662
|
+
import os2 from "os";
|
|
1663
|
+
import fs7 from "fs/promises";
|
|
2036
1664
|
import { execFile as execFile2 } from "child_process";
|
|
2037
1665
|
import { promisify as promisify2 } from "util";
|
|
2038
1666
|
|
|
2039
1667
|
// src/utils/MergeRequestHelper.ts
|
|
2040
|
-
import
|
|
2041
|
-
import
|
|
1668
|
+
import fs5 from "fs";
|
|
1669
|
+
import path6 from "path";
|
|
2042
1670
|
var TAPD_PATTERNS = [
|
|
2043
1671
|
/--story=(\d+)/i,
|
|
2044
1672
|
/--bug=(\d+)/i,
|
|
@@ -2082,9 +1710,9 @@ function generateMRDescription(options) {
|
|
|
2082
1710
|
];
|
|
2083
1711
|
const planSections = [];
|
|
2084
1712
|
for (const { filename, label } of summaryFiles) {
|
|
2085
|
-
const filePath =
|
|
2086
|
-
if (
|
|
2087
|
-
const content =
|
|
1713
|
+
const filePath = path6.join(planDir2, ".claude-plan", `issue-${issueIid}`, filename);
|
|
1714
|
+
if (fs5.existsSync(filePath)) {
|
|
1715
|
+
const content = fs5.readFileSync(filePath, "utf-8");
|
|
2088
1716
|
const summary = extractSummary(content);
|
|
2089
1717
|
if (summary) {
|
|
2090
1718
|
planSections.push(`### ${label}
|
|
@@ -2108,7 +1736,7 @@ function extractSummary(content, maxLines = 20) {
|
|
|
2108
1736
|
|
|
2109
1737
|
// src/deploy/PortAllocator.ts
|
|
2110
1738
|
import net from "net";
|
|
2111
|
-
var
|
|
1739
|
+
var logger6 = logger.child("PortAllocator");
|
|
2112
1740
|
var DEFAULT_OPTIONS = {
|
|
2113
1741
|
backendPortBase: 4e3,
|
|
2114
1742
|
frontendPortBase: 9e3,
|
|
@@ -2133,7 +1761,7 @@ var PortAllocator = class {
|
|
|
2133
1761
|
async allocate(issueIid) {
|
|
2134
1762
|
const existing = this.allocated.get(issueIid);
|
|
2135
1763
|
if (existing) {
|
|
2136
|
-
|
|
1764
|
+
logger6.info("Returning already allocated ports", { issueIid, ports: existing });
|
|
2137
1765
|
return existing;
|
|
2138
1766
|
}
|
|
2139
1767
|
const usedBackend = new Set([...this.allocated.values()].map((p) => p.backendPort));
|
|
@@ -2151,10 +1779,10 @@ var PortAllocator = class {
|
|
|
2151
1779
|
if (beOk && feOk) {
|
|
2152
1780
|
const pair = { backendPort, frontendPort };
|
|
2153
1781
|
this.allocated.set(issueIid, pair);
|
|
2154
|
-
|
|
1782
|
+
logger6.info("Ports allocated", { issueIid, ...pair });
|
|
2155
1783
|
return pair;
|
|
2156
1784
|
}
|
|
2157
|
-
|
|
1785
|
+
logger6.debug("Port pair unavailable, trying next", {
|
|
2158
1786
|
backendPort,
|
|
2159
1787
|
frontendPort,
|
|
2160
1788
|
beOk,
|
|
@@ -2169,7 +1797,7 @@ var PortAllocator = class {
|
|
|
2169
1797
|
const pair = this.allocated.get(issueIid);
|
|
2170
1798
|
if (pair) {
|
|
2171
1799
|
this.allocated.delete(issueIid);
|
|
2172
|
-
|
|
1800
|
+
logger6.info("Ports released", { issueIid, ...pair });
|
|
2173
1801
|
}
|
|
2174
1802
|
}
|
|
2175
1803
|
getPortsForIssue(issueIid) {
|
|
@@ -2180,16 +1808,16 @@ var PortAllocator = class {
|
|
|
2180
1808
|
}
|
|
2181
1809
|
restore(issueIid, ports) {
|
|
2182
1810
|
this.allocated.set(issueIid, ports);
|
|
2183
|
-
|
|
1811
|
+
logger6.info("Ports restored from persistence", { issueIid, ...ports });
|
|
2184
1812
|
}
|
|
2185
1813
|
};
|
|
2186
1814
|
|
|
2187
1815
|
// src/deploy/DevServerManager.ts
|
|
2188
|
-
import { spawn
|
|
2189
|
-
import
|
|
1816
|
+
import { spawn } from "child_process";
|
|
1817
|
+
import path7 from "path";
|
|
2190
1818
|
import https from "https";
|
|
2191
1819
|
import http from "http";
|
|
2192
|
-
var
|
|
1820
|
+
var logger7 = logger.child("DevServerManager");
|
|
2193
1821
|
var DEFAULT_OPTIONS2 = {
|
|
2194
1822
|
healthCheckTimeoutMs: 12e4,
|
|
2195
1823
|
healthCheckIntervalMs: 3e3
|
|
@@ -2234,51 +1862,51 @@ var DevServerManager = class {
|
|
|
2234
1862
|
}
|
|
2235
1863
|
async startServers(wtCtx, ports) {
|
|
2236
1864
|
if (this.servers.has(wtCtx.issueIid)) {
|
|
2237
|
-
|
|
1865
|
+
logger7.info("Servers already running for issue", { issueIid: wtCtx.issueIid });
|
|
2238
1866
|
return;
|
|
2239
1867
|
}
|
|
2240
|
-
|
|
1868
|
+
logger7.info("Starting dev servers", { issueIid: wtCtx.issueIid, ...ports });
|
|
2241
1869
|
const backendEnv = {
|
|
2242
1870
|
...process.env,
|
|
2243
1871
|
PORT: String(ports.backendPort),
|
|
2244
1872
|
E2E_PORT_OVERRIDE: "1",
|
|
2245
1873
|
ENV_PATH: ".env.development.local"
|
|
2246
1874
|
};
|
|
2247
|
-
const backend =
|
|
1875
|
+
const backend = spawn("node", ["ace", "serve", "--watch"], {
|
|
2248
1876
|
cwd: wtCtx.workDir,
|
|
2249
1877
|
env: backendEnv,
|
|
2250
1878
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2251
1879
|
detached: false
|
|
2252
1880
|
});
|
|
2253
1881
|
backend.stdout?.on("data", (data) => {
|
|
2254
|
-
|
|
1882
|
+
logger7.debug(`[BE #${wtCtx.issueIid}] ${data.toString().trimEnd()}`);
|
|
2255
1883
|
});
|
|
2256
1884
|
backend.stderr?.on("data", (data) => {
|
|
2257
|
-
|
|
1885
|
+
logger7.debug(`[BE #${wtCtx.issueIid} ERR] ${data.toString().trimEnd()}`);
|
|
2258
1886
|
});
|
|
2259
1887
|
backend.on("exit", (code) => {
|
|
2260
|
-
|
|
1888
|
+
logger7.info("Backend process exited", { issueIid: wtCtx.issueIid, code });
|
|
2261
1889
|
});
|
|
2262
|
-
const frontendDir =
|
|
1890
|
+
const frontendDir = path7.join(wtCtx.workDir, "frontend");
|
|
2263
1891
|
const frontendEnv = {
|
|
2264
1892
|
...process.env,
|
|
2265
1893
|
BACKEND_PORT: String(ports.backendPort),
|
|
2266
1894
|
FRONTEND_PORT: String(ports.frontendPort)
|
|
2267
1895
|
};
|
|
2268
|
-
const frontend =
|
|
1896
|
+
const frontend = spawn("pnpm", ["dev", "--", "--port", String(ports.frontendPort)], {
|
|
2269
1897
|
cwd: frontendDir,
|
|
2270
1898
|
env: frontendEnv,
|
|
2271
1899
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2272
1900
|
detached: false
|
|
2273
1901
|
});
|
|
2274
1902
|
frontend.stdout?.on("data", (data) => {
|
|
2275
|
-
|
|
1903
|
+
logger7.debug(`[FE #${wtCtx.issueIid}] ${data.toString().trimEnd()}`);
|
|
2276
1904
|
});
|
|
2277
1905
|
frontend.stderr?.on("data", (data) => {
|
|
2278
|
-
|
|
1906
|
+
logger7.debug(`[FE #${wtCtx.issueIid} ERR] ${data.toString().trimEnd()}`);
|
|
2279
1907
|
});
|
|
2280
1908
|
frontend.on("exit", (code) => {
|
|
2281
|
-
|
|
1909
|
+
logger7.info("Frontend process exited", { issueIid: wtCtx.issueIid, code });
|
|
2282
1910
|
});
|
|
2283
1911
|
const serverSet = {
|
|
2284
1912
|
backend,
|
|
@@ -2288,7 +1916,7 @@ var DevServerManager = class {
|
|
|
2288
1916
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2289
1917
|
};
|
|
2290
1918
|
this.servers.set(wtCtx.issueIid, serverSet);
|
|
2291
|
-
|
|
1919
|
+
logger7.info("Waiting for servers to become healthy", { issueIid: wtCtx.issueIid });
|
|
2292
1920
|
try {
|
|
2293
1921
|
await Promise.all([
|
|
2294
1922
|
waitForPort(
|
|
@@ -2304,9 +1932,9 @@ var DevServerManager = class {
|
|
|
2304
1932
|
this.options.healthCheckIntervalMs
|
|
2305
1933
|
)
|
|
2306
1934
|
]);
|
|
2307
|
-
|
|
1935
|
+
logger7.info("Dev servers healthy", { issueIid: wtCtx.issueIid, ...ports });
|
|
2308
1936
|
} catch (err) {
|
|
2309
|
-
|
|
1937
|
+
logger7.error("Dev servers failed health check, cleaning up", {
|
|
2310
1938
|
issueIid: wtCtx.issueIid,
|
|
2311
1939
|
error: err.message
|
|
2312
1940
|
});
|
|
@@ -2317,7 +1945,7 @@ var DevServerManager = class {
|
|
|
2317
1945
|
stopServers(issueIid) {
|
|
2318
1946
|
const set = this.servers.get(issueIid);
|
|
2319
1947
|
if (!set) return;
|
|
2320
|
-
|
|
1948
|
+
logger7.info("Stopping dev servers", { issueIid, ports: set.ports });
|
|
2321
1949
|
killProcess(set.backend, `backend #${issueIid}`);
|
|
2322
1950
|
killProcess(set.frontend, `frontend #${issueIid}`);
|
|
2323
1951
|
this.servers.delete(issueIid);
|
|
@@ -2346,23 +1974,23 @@ function killProcess(proc, label) {
|
|
|
2346
1974
|
proc.kill("SIGTERM");
|
|
2347
1975
|
setTimeout(() => {
|
|
2348
1976
|
if (!proc.killed && proc.exitCode === null) {
|
|
2349
|
-
|
|
1977
|
+
logger7.warn(`Force killing ${label}`);
|
|
2350
1978
|
proc.kill("SIGKILL");
|
|
2351
1979
|
}
|
|
2352
1980
|
}, 5e3);
|
|
2353
1981
|
} catch (err) {
|
|
2354
|
-
|
|
1982
|
+
logger7.warn(`Failed to kill ${label}`, { error: err.message });
|
|
2355
1983
|
}
|
|
2356
1984
|
}
|
|
2357
1985
|
|
|
2358
1986
|
// src/e2e/ScreenshotCollector.ts
|
|
2359
|
-
import
|
|
2360
|
-
import
|
|
2361
|
-
var
|
|
1987
|
+
import fs6 from "fs";
|
|
1988
|
+
import path8 from "path";
|
|
1989
|
+
var logger8 = logger.child("ScreenshotCollector");
|
|
2362
1990
|
var MAX_SCREENSHOTS = 20;
|
|
2363
1991
|
function walkDir(dir, files = []) {
|
|
2364
|
-
for (const entry of
|
|
2365
|
-
const full =
|
|
1992
|
+
for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
|
|
1993
|
+
const full = path8.join(dir, entry.name);
|
|
2366
1994
|
if (entry.isDirectory()) {
|
|
2367
1995
|
walkDir(full, files);
|
|
2368
1996
|
} else if (entry.isFile() && entry.name.endsWith(".png")) {
|
|
@@ -2372,34 +2000,34 @@ function walkDir(dir, files = []) {
|
|
|
2372
2000
|
return files;
|
|
2373
2001
|
}
|
|
2374
2002
|
function collectScreenshots(workDir) {
|
|
2375
|
-
const testResultsDir =
|
|
2376
|
-
if (!
|
|
2377
|
-
|
|
2003
|
+
const testResultsDir = path8.join(workDir, "frontend", "test-results");
|
|
2004
|
+
if (!fs6.existsSync(testResultsDir)) {
|
|
2005
|
+
logger8.debug("test-results directory not found", { dir: testResultsDir });
|
|
2378
2006
|
return [];
|
|
2379
2007
|
}
|
|
2380
2008
|
const pngFiles = walkDir(testResultsDir);
|
|
2381
2009
|
if (pngFiles.length === 0) {
|
|
2382
|
-
|
|
2010
|
+
logger8.debug("No screenshots found");
|
|
2383
2011
|
return [];
|
|
2384
2012
|
}
|
|
2385
2013
|
const screenshots = pngFiles.map((filePath) => {
|
|
2386
|
-
const relative =
|
|
2387
|
-
const testName = relative.split(
|
|
2014
|
+
const relative = path8.relative(testResultsDir, filePath);
|
|
2015
|
+
const testName = relative.split(path8.sep)[0] || path8.basename(filePath, ".png");
|
|
2388
2016
|
return { filePath, testName };
|
|
2389
2017
|
});
|
|
2390
2018
|
if (screenshots.length > MAX_SCREENSHOTS) {
|
|
2391
|
-
|
|
2019
|
+
logger8.warn("Too many screenshots, truncating", {
|
|
2392
2020
|
total: screenshots.length,
|
|
2393
2021
|
max: MAX_SCREENSHOTS
|
|
2394
2022
|
});
|
|
2395
2023
|
return screenshots.slice(0, MAX_SCREENSHOTS);
|
|
2396
2024
|
}
|
|
2397
|
-
|
|
2025
|
+
logger8.info("Screenshots collected", { count: screenshots.length });
|
|
2398
2026
|
return screenshots;
|
|
2399
2027
|
}
|
|
2400
2028
|
|
|
2401
2029
|
// src/e2e/ScreenshotPublisher.ts
|
|
2402
|
-
var
|
|
2030
|
+
var logger9 = logger.child("ScreenshotPublisher");
|
|
2403
2031
|
function buildComment(uploaded, truncated) {
|
|
2404
2032
|
const lines = [t("screenshot.title"), ""];
|
|
2405
2033
|
for (const item of uploaded) {
|
|
@@ -2418,12 +2046,12 @@ var ScreenshotPublisher = class {
|
|
|
2418
2046
|
const { workDir, issueIid, issueId, mrIid } = options;
|
|
2419
2047
|
const screenshots = collectScreenshots(workDir);
|
|
2420
2048
|
if (screenshots.length === 0) {
|
|
2421
|
-
|
|
2049
|
+
logger9.info("No E2E screenshots to publish", { issueIid });
|
|
2422
2050
|
return;
|
|
2423
2051
|
}
|
|
2424
2052
|
const uploaded = await this.uploadAll(screenshots);
|
|
2425
2053
|
if (uploaded.length === 0) {
|
|
2426
|
-
|
|
2054
|
+
logger9.warn("All screenshot uploads failed", { issueIid });
|
|
2427
2055
|
return;
|
|
2428
2056
|
}
|
|
2429
2057
|
const truncated = screenshots.length >= 20;
|
|
@@ -2432,7 +2060,7 @@ var ScreenshotPublisher = class {
|
|
|
2432
2060
|
if (mrIid) {
|
|
2433
2061
|
await this.postToMergeRequest(mrIid, comment);
|
|
2434
2062
|
}
|
|
2435
|
-
|
|
2063
|
+
logger9.info("E2E screenshots published", {
|
|
2436
2064
|
issueIid,
|
|
2437
2065
|
mrIid,
|
|
2438
2066
|
count: uploaded.length
|
|
@@ -2448,7 +2076,7 @@ var ScreenshotPublisher = class {
|
|
|
2448
2076
|
markdown: result.markdown
|
|
2449
2077
|
});
|
|
2450
2078
|
} catch (err) {
|
|
2451
|
-
|
|
2079
|
+
logger9.warn("Failed to upload screenshot", {
|
|
2452
2080
|
filePath: screenshot.filePath,
|
|
2453
2081
|
error: err.message
|
|
2454
2082
|
});
|
|
@@ -2460,7 +2088,7 @@ var ScreenshotPublisher = class {
|
|
|
2460
2088
|
try {
|
|
2461
2089
|
await this.gongfeng.createIssueNote(issueId, comment);
|
|
2462
2090
|
} catch (err) {
|
|
2463
|
-
|
|
2091
|
+
logger9.warn("Failed to post screenshots to issue", {
|
|
2464
2092
|
issueId,
|
|
2465
2093
|
error: err.message
|
|
2466
2094
|
});
|
|
@@ -2470,7 +2098,7 @@ var ScreenshotPublisher = class {
|
|
|
2470
2098
|
try {
|
|
2471
2099
|
await this.gongfeng.createMergeRequestNote(mrIid, comment);
|
|
2472
2100
|
} catch (err) {
|
|
2473
|
-
|
|
2101
|
+
logger9.warn("Failed to post screenshots to merge request", {
|
|
2474
2102
|
mrIid,
|
|
2475
2103
|
error: err.message
|
|
2476
2104
|
});
|
|
@@ -2480,7 +2108,7 @@ var ScreenshotPublisher = class {
|
|
|
2480
2108
|
|
|
2481
2109
|
// src/orchestrator/PipelineOrchestrator.ts
|
|
2482
2110
|
var execFileAsync2 = promisify2(execFile2);
|
|
2483
|
-
var
|
|
2111
|
+
var logger10 = logger.child("PipelineOrchestrator");
|
|
2484
2112
|
var PipelineOrchestrator = class {
|
|
2485
2113
|
config;
|
|
2486
2114
|
gongfeng;
|
|
@@ -2502,7 +2130,7 @@ var PipelineOrchestrator = class {
|
|
|
2502
2130
|
this.supplementStore = supplementStore;
|
|
2503
2131
|
const mode = resolvePipelineMode(config.ai.mode, config.pipeline?.mode === "auto" ? void 0 : config.pipeline?.mode);
|
|
2504
2132
|
this.pipelineDef = getPipelineDef(mode);
|
|
2505
|
-
|
|
2133
|
+
logger10.info("Pipeline mode resolved", { mode: this.pipelineDef.mode, aiMode: config.ai.mode });
|
|
2506
2134
|
this.portAllocator = new PortAllocator({
|
|
2507
2135
|
backendPortBase: config.e2e.backendPortBase,
|
|
2508
2136
|
frontendPortBase: config.e2e.frontendPortBase
|
|
@@ -2517,6 +2145,44 @@ var PipelineOrchestrator = class {
|
|
|
2517
2145
|
getDevServerManager() {
|
|
2518
2146
|
return this.devServerManager;
|
|
2519
2147
|
}
|
|
2148
|
+
async cleanupStaleState() {
|
|
2149
|
+
logger10.info("Cleaning up stale worktree state...");
|
|
2150
|
+
let cleaned = 0;
|
|
2151
|
+
try {
|
|
2152
|
+
const worktrees = await this.mainGit.worktreeList();
|
|
2153
|
+
for (const wtDir of worktrees) {
|
|
2154
|
+
if (wtDir === this.config.project.gitRootDir) continue;
|
|
2155
|
+
if (!wtDir.includes("/issue-")) continue;
|
|
2156
|
+
try {
|
|
2157
|
+
const wtGit = new GitOperations(wtDir);
|
|
2158
|
+
if (await wtGit.isRebaseInProgress()) {
|
|
2159
|
+
logger10.warn("Aborting residual rebase in worktree", { dir: wtDir });
|
|
2160
|
+
await wtGit.rebaseAbort();
|
|
2161
|
+
cleaned++;
|
|
2162
|
+
}
|
|
2163
|
+
const indexLock = path9.join(wtDir, ".git", "index.lock");
|
|
2164
|
+
try {
|
|
2165
|
+
await fs7.unlink(indexLock);
|
|
2166
|
+
logger10.warn("Removed stale index.lock", { path: indexLock });
|
|
2167
|
+
cleaned++;
|
|
2168
|
+
} catch {
|
|
2169
|
+
}
|
|
2170
|
+
} catch (err) {
|
|
2171
|
+
logger10.warn("Failed to clean worktree state", { dir: wtDir, error: err.message });
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
} catch (err) {
|
|
2175
|
+
logger10.warn("Failed to list worktrees for cleanup", { error: err.message });
|
|
2176
|
+
}
|
|
2177
|
+
const mainIndexLock = path9.join(this.config.project.gitRootDir, ".git", "index.lock");
|
|
2178
|
+
try {
|
|
2179
|
+
await fs7.unlink(mainIndexLock);
|
|
2180
|
+
logger10.warn("Removed stale main repo index.lock", { path: mainIndexLock });
|
|
2181
|
+
cleaned++;
|
|
2182
|
+
} catch {
|
|
2183
|
+
}
|
|
2184
|
+
logger10.info("Stale state cleanup complete", { cleaned });
|
|
2185
|
+
}
|
|
2520
2186
|
restorePortAllocations() {
|
|
2521
2187
|
for (const record of this.tracker.getAll()) {
|
|
2522
2188
|
if (record.ports) {
|
|
@@ -2531,14 +2197,14 @@ var PipelineOrchestrator = class {
|
|
|
2531
2197
|
eventBus.emitTyped("pipeline:progress", { issueIid, step, message });
|
|
2532
2198
|
}
|
|
2533
2199
|
computeWorktreeContext(issueIid, branchName) {
|
|
2534
|
-
const gitRootDir =
|
|
2535
|
-
const workDir =
|
|
2200
|
+
const gitRootDir = path9.join(this.config.project.worktreeBaseDir, `issue-${issueIid}`);
|
|
2201
|
+
const workDir = path9.join(gitRootDir, this.config.project.projectSubDir);
|
|
2536
2202
|
return { gitRootDir, workDir, branchName, issueIid };
|
|
2537
2203
|
}
|
|
2538
2204
|
async ensureWorktree(wtCtx) {
|
|
2539
2205
|
const worktrees = await this.mainGit.worktreeList();
|
|
2540
2206
|
if (worktrees.includes(wtCtx.gitRootDir)) {
|
|
2541
|
-
|
|
2207
|
+
logger10.info("Reusing existing worktree", { dir: wtCtx.gitRootDir });
|
|
2542
2208
|
return;
|
|
2543
2209
|
}
|
|
2544
2210
|
const localExists = await this.mainGit.branchExists(wtCtx.branchName);
|
|
@@ -2560,69 +2226,84 @@ var PipelineOrchestrator = class {
|
|
|
2560
2226
|
async cleanupWorktree(wtCtx) {
|
|
2561
2227
|
try {
|
|
2562
2228
|
await this.mainGit.worktreeRemove(wtCtx.gitRootDir, true);
|
|
2563
|
-
|
|
2229
|
+
logger10.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
|
|
2564
2230
|
} catch (err) {
|
|
2565
|
-
|
|
2231
|
+
logger10.warn("Failed to cleanup worktree", { dir: wtCtx.gitRootDir, error: err.message });
|
|
2566
2232
|
}
|
|
2567
2233
|
}
|
|
2568
2234
|
async installDependencies(workDir) {
|
|
2569
|
-
|
|
2570
|
-
const
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2235
|
+
logger10.info("Installing dependencies in worktree", { workDir });
|
|
2236
|
+
const knowledge = getProjectKnowledge() ?? KNOWLEDGE_DEFAULTS;
|
|
2237
|
+
const pkgMgr = knowledge.toolchain.packageManager.toLowerCase();
|
|
2238
|
+
const isNodeProject = ["npm", "pnpm", "yarn", "bun"].some((m) => pkgMgr.includes(m));
|
|
2239
|
+
if (isNodeProject) {
|
|
2240
|
+
const ready = await this.ensureNodeModules(workDir);
|
|
2241
|
+
if (ready) {
|
|
2242
|
+
logger10.info("node_modules ready \u2014 skipping install");
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2574
2245
|
}
|
|
2246
|
+
const installCmd = knowledge.toolchain.installCommand;
|
|
2247
|
+
const fallbackCmd = knowledge.toolchain.installFallbackCommand;
|
|
2248
|
+
const [bin, ...args] = installCmd.split(/\s+/);
|
|
2575
2249
|
try {
|
|
2576
|
-
await execFileAsync2(
|
|
2250
|
+
await execFileAsync2(bin, args, {
|
|
2577
2251
|
cwd: workDir,
|
|
2578
2252
|
maxBuffer: 10 * 1024 * 1024,
|
|
2579
2253
|
timeout: 3e5
|
|
2580
2254
|
});
|
|
2581
|
-
|
|
2255
|
+
logger10.info("Dependencies installed");
|
|
2582
2256
|
} catch (err) {
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
try {
|
|
2587
|
-
await execFileAsync2("pnpm", ["install", "--frozen-lockfile", "--ignore-scripts"], {
|
|
2588
|
-
cwd: workDir,
|
|
2589
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
2590
|
-
timeout: 3e5
|
|
2257
|
+
if (fallbackCmd) {
|
|
2258
|
+
logger10.warn(`${installCmd} failed, retrying with fallback command`, {
|
|
2259
|
+
error: err.message
|
|
2591
2260
|
});
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2261
|
+
const [fallbackBin, ...fallbackArgs] = fallbackCmd.split(/\s+/);
|
|
2262
|
+
try {
|
|
2263
|
+
await execFileAsync2(fallbackBin, fallbackArgs, {
|
|
2264
|
+
cwd: workDir,
|
|
2265
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
2266
|
+
timeout: 3e5
|
|
2267
|
+
});
|
|
2268
|
+
logger10.info("Dependencies installed (fallback)");
|
|
2269
|
+
} catch (retryErr) {
|
|
2270
|
+
logger10.warn("Fallback install also failed", {
|
|
2271
|
+
error: retryErr.message
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
} else {
|
|
2275
|
+
logger10.warn("Install failed, no fallback configured", {
|
|
2276
|
+
error: err.message
|
|
2596
2277
|
});
|
|
2597
2278
|
}
|
|
2598
2279
|
}
|
|
2599
2280
|
}
|
|
2600
|
-
async
|
|
2601
|
-
const targetBin =
|
|
2281
|
+
async ensureNodeModules(workDir) {
|
|
2282
|
+
const targetBin = path9.join(workDir, "node_modules", ".bin");
|
|
2602
2283
|
try {
|
|
2603
|
-
await
|
|
2604
|
-
|
|
2605
|
-
return
|
|
2284
|
+
await fs7.access(targetBin);
|
|
2285
|
+
logger10.info("node_modules already complete (has .bin/)");
|
|
2286
|
+
return true;
|
|
2606
2287
|
} catch {
|
|
2607
2288
|
}
|
|
2608
|
-
const sourceNM =
|
|
2609
|
-
const targetNM =
|
|
2289
|
+
const sourceNM = path9.join(this.config.project.workDir, "node_modules");
|
|
2290
|
+
const targetNM = path9.join(workDir, "node_modules");
|
|
2610
2291
|
try {
|
|
2611
|
-
await
|
|
2292
|
+
await fs7.access(sourceNM);
|
|
2612
2293
|
} catch {
|
|
2613
|
-
|
|
2294
|
+
logger10.warn("Main repo node_modules not found, skipping seed", { sourceNM });
|
|
2614
2295
|
return false;
|
|
2615
2296
|
}
|
|
2616
|
-
|
|
2297
|
+
logger10.info("Seeding node_modules from main repo via reflink copy", { sourceNM, targetNM });
|
|
2617
2298
|
try {
|
|
2618
2299
|
await execFileAsync2("rm", ["-rf", targetNM], { timeout: 6e4 });
|
|
2619
2300
|
await execFileAsync2("cp", ["-a", "--reflink=auto", sourceNM, targetNM], {
|
|
2620
2301
|
timeout: 12e4
|
|
2621
2302
|
});
|
|
2622
|
-
|
|
2303
|
+
logger10.info("node_modules seeded from main repo");
|
|
2623
2304
|
return true;
|
|
2624
2305
|
} catch (err) {
|
|
2625
|
-
|
|
2306
|
+
logger10.warn("Failed to seed node_modules from main repo", {
|
|
2626
2307
|
error: err.message
|
|
2627
2308
|
});
|
|
2628
2309
|
return false;
|
|
@@ -2632,13 +2313,14 @@ var PipelineOrchestrator = class {
|
|
|
2632
2313
|
const record = this.tracker.get(issueIid);
|
|
2633
2314
|
if (!record) throw new Error(`Issue ${issueIid} not found in tracker`);
|
|
2634
2315
|
const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
|
|
2635
|
-
|
|
2316
|
+
logger10.info("Restarting issue \u2014 cleaning context", { issueIid, branchName: record.branchName });
|
|
2317
|
+
this.aiRunner.killByWorkDir(wtCtx.workDir);
|
|
2636
2318
|
this.stopPreviewServers(issueIid);
|
|
2637
2319
|
try {
|
|
2638
2320
|
const deleted = await this.gongfeng.cleanupAgentNotes(record.issueId);
|
|
2639
|
-
|
|
2321
|
+
logger10.info("Agent notes cleaned up", { issueIid, deleted });
|
|
2640
2322
|
} catch (err) {
|
|
2641
|
-
|
|
2323
|
+
logger10.warn("Failed to cleanup agent notes", { issueIid, error: err.message });
|
|
2642
2324
|
}
|
|
2643
2325
|
await this.mainGitMutex.runExclusive(async () => {
|
|
2644
2326
|
await this.cleanupWorktree(wtCtx);
|
|
@@ -2652,7 +2334,7 @@ var PipelineOrchestrator = class {
|
|
|
2652
2334
|
}
|
|
2653
2335
|
});
|
|
2654
2336
|
this.tracker.resetFull(issueIid);
|
|
2655
|
-
|
|
2337
|
+
logger10.info("Issue restarted", { issueIid });
|
|
2656
2338
|
}
|
|
2657
2339
|
retryFromPhase(issueIid, phase) {
|
|
2658
2340
|
const record = this.tracker.get(issueIid);
|
|
@@ -2662,7 +2344,7 @@ var PipelineOrchestrator = class {
|
|
|
2662
2344
|
if (!spec || spec.kind !== "ai") {
|
|
2663
2345
|
throw new Error(`Invalid phase for retry: ${phase}`);
|
|
2664
2346
|
}
|
|
2665
|
-
|
|
2347
|
+
logger10.info("Retrying issue from phase", { issueIid, phase });
|
|
2666
2348
|
this.tracker.resetToPhase(issueIid, phase, issueDef);
|
|
2667
2349
|
}
|
|
2668
2350
|
getIssueSpecificPipelineDef(record) {
|
|
@@ -2674,7 +2356,7 @@ var PipelineOrchestrator = class {
|
|
|
2674
2356
|
async processIssue(issue) {
|
|
2675
2357
|
const branchName = `${this.config.project.branchPrefix}-${issue.iid}`;
|
|
2676
2358
|
const wtCtx = this.computeWorktreeContext(issue.iid, branchName);
|
|
2677
|
-
|
|
2359
|
+
logger10.info("Processing issue", { iid: issue.iid, title: issue.title, branchName, worktree: wtCtx.gitRootDir });
|
|
2678
2360
|
let record = this.tracker.get(issue.iid);
|
|
2679
2361
|
const isRetry = record?.state === "failed" /* Failed */;
|
|
2680
2362
|
if (!record) {
|
|
@@ -2698,7 +2380,7 @@ var PipelineOrchestrator = class {
|
|
|
2698
2380
|
"auto-finish:processing"
|
|
2699
2381
|
]);
|
|
2700
2382
|
} catch (err) {
|
|
2701
|
-
|
|
2383
|
+
logger10.warn("Failed to update issue labels", { error: err.message });
|
|
2702
2384
|
}
|
|
2703
2385
|
try {
|
|
2704
2386
|
await this.gongfeng.createIssueNote(
|
|
@@ -2751,10 +2433,13 @@ var PipelineOrchestrator = class {
|
|
|
2751
2433
|
const needsDeployment = this.shouldDeployServers(issue.iid);
|
|
2752
2434
|
let serversStarted = false;
|
|
2753
2435
|
for (let i = startIdx; i < issuePipelineDef.phases.length; i++) {
|
|
2436
|
+
if (isShuttingDown()) {
|
|
2437
|
+
throw new Error("Service shutting down");
|
|
2438
|
+
}
|
|
2754
2439
|
const spec = issuePipelineDef.phases[i];
|
|
2755
2440
|
if (spec.kind === "gate") {
|
|
2756
2441
|
if (this.shouldAutoApprove(issue.labels)) {
|
|
2757
|
-
|
|
2442
|
+
logger10.info("Auto-approving review gate (matched autoApproveLabels)", {
|
|
2758
2443
|
iid: issue.iid,
|
|
2759
2444
|
labels: issue.labels,
|
|
2760
2445
|
autoApproveLabels: this.config.review.autoApproveLabels
|
|
@@ -2775,7 +2460,7 @@ var PipelineOrchestrator = class {
|
|
|
2775
2460
|
this.tracker.updateState(issue.iid, spec.startState);
|
|
2776
2461
|
wtPlan.updatePhaseProgress(spec.name, "in_progress");
|
|
2777
2462
|
eventBus.emitTyped("review:requested", { issueIid: issue.iid });
|
|
2778
|
-
|
|
2463
|
+
logger10.info("Review gate reached, pausing", { iid: issue.iid });
|
|
2779
2464
|
return;
|
|
2780
2465
|
}
|
|
2781
2466
|
const phase = createPhase(spec.name, this.aiRunner, wtGit, wtPlan, this.gongfeng, this.tracker, this.config);
|
|
@@ -2811,7 +2496,7 @@ var PipelineOrchestrator = class {
|
|
|
2811
2496
|
mrIid: mrResult?.iid
|
|
2812
2497
|
});
|
|
2813
2498
|
} catch (err) {
|
|
2814
|
-
|
|
2499
|
+
logger10.warn("Failed to publish E2E screenshots", {
|
|
2815
2500
|
iid: issue.iid,
|
|
2816
2501
|
error: err.message
|
|
2817
2502
|
});
|
|
@@ -2828,20 +2513,25 @@ var PipelineOrchestrator = class {
|
|
|
2828
2513
|
} catch {
|
|
2829
2514
|
}
|
|
2830
2515
|
if (serversStarted && this.config.preview.keepAfterComplete) {
|
|
2831
|
-
|
|
2516
|
+
logger10.info("Preview servers kept running after completion", { iid: issue.iid });
|
|
2832
2517
|
} else {
|
|
2833
2518
|
this.stopPreviewServers(issue.iid);
|
|
2834
2519
|
await this.mainGitMutex.runExclusive(() => this.cleanupWorktree(wtCtx));
|
|
2835
2520
|
}
|
|
2836
|
-
|
|
2521
|
+
logger10.info("Issue processing completed", { iid: issue.iid });
|
|
2837
2522
|
} catch (err) {
|
|
2838
2523
|
const errorMsg = err.message;
|
|
2839
|
-
|
|
2524
|
+
logger10.error("Issue processing failed", { iid: issue.iid, error: errorMsg });
|
|
2840
2525
|
const currentRecord = this.tracker.get(issue.iid);
|
|
2841
2526
|
const failedAtState = currentRecord?.state || "pending" /* Pending */;
|
|
2842
|
-
|
|
2527
|
+
const wasReset = failedAtState === "pending" /* Pending */ && currentRecord?.attempts === 0;
|
|
2528
|
+
if (failedAtState !== "failed" /* Failed */ && !wasReset) {
|
|
2843
2529
|
this.tracker.markFailed(issue.iid, errorMsg.slice(0, 500), failedAtState);
|
|
2844
2530
|
}
|
|
2531
|
+
if (wasReset) {
|
|
2532
|
+
logger10.info("Issue was reset during processing, skipping failure marking", { iid: issue.iid });
|
|
2533
|
+
throw err;
|
|
2534
|
+
}
|
|
2845
2535
|
try {
|
|
2846
2536
|
await this.gongfeng.updateIssueLabels(issue.id, [
|
|
2847
2537
|
...issue.labels.filter((l) => !l.startsWith("auto-finish:") && l !== "auto-finish"),
|
|
@@ -2857,7 +2547,7 @@ var PipelineOrchestrator = class {
|
|
|
2857
2547
|
);
|
|
2858
2548
|
} catch {
|
|
2859
2549
|
}
|
|
2860
|
-
|
|
2550
|
+
logger10.info("Worktree preserved for debugging", { dir: wtCtx.gitRootDir });
|
|
2861
2551
|
throw err;
|
|
2862
2552
|
}
|
|
2863
2553
|
}
|
|
@@ -2886,7 +2576,7 @@ var PipelineOrchestrator = class {
|
|
|
2886
2576
|
title,
|
|
2887
2577
|
description
|
|
2888
2578
|
});
|
|
2889
|
-
|
|
2579
|
+
logger10.info("Merge request created successfully", {
|
|
2890
2580
|
iid: issue.iid,
|
|
2891
2581
|
mrIid: mr.iid,
|
|
2892
2582
|
mrUrl: mr.web_url
|
|
@@ -2894,7 +2584,7 @@ var PipelineOrchestrator = class {
|
|
|
2894
2584
|
return { url: mr.web_url, iid: mr.iid };
|
|
2895
2585
|
} catch (err) {
|
|
2896
2586
|
const errorMsg = err.message;
|
|
2897
|
-
|
|
2587
|
+
logger10.warn("Failed to create merge request, trying to find existing one", {
|
|
2898
2588
|
iid: issue.iid,
|
|
2899
2589
|
error: errorMsg
|
|
2900
2590
|
});
|
|
@@ -2911,7 +2601,7 @@ var PipelineOrchestrator = class {
|
|
|
2911
2601
|
this.config.project.baseBranch
|
|
2912
2602
|
);
|
|
2913
2603
|
if (existing) {
|
|
2914
|
-
|
|
2604
|
+
logger10.info("Found existing merge request", {
|
|
2915
2605
|
iid: issueIid,
|
|
2916
2606
|
mrIid: existing.iid,
|
|
2917
2607
|
mrUrl: existing.web_url
|
|
@@ -2919,7 +2609,7 @@ var PipelineOrchestrator = class {
|
|
|
2919
2609
|
return { url: existing.web_url, iid: existing.iid };
|
|
2920
2610
|
}
|
|
2921
2611
|
} catch (findErr) {
|
|
2922
|
-
|
|
2612
|
+
logger10.warn("Failed to find existing merge request", {
|
|
2923
2613
|
iid: issueIid,
|
|
2924
2614
|
error: findErr.message
|
|
2925
2615
|
});
|
|
@@ -2978,7 +2668,7 @@ var PipelineOrchestrator = class {
|
|
|
2978
2668
|
});
|
|
2979
2669
|
return ports;
|
|
2980
2670
|
} catch (err) {
|
|
2981
|
-
|
|
2671
|
+
logger10.error("Failed to start preview servers", {
|
|
2982
2672
|
iid: issue.iid,
|
|
2983
2673
|
error: err.message
|
|
2984
2674
|
});
|
|
@@ -2998,7 +2688,7 @@ var PipelineOrchestrator = class {
|
|
|
2998
2688
|
}
|
|
2999
2689
|
getPreviewHost() {
|
|
3000
2690
|
if (this.config.preview.host) return this.config.preview.host;
|
|
3001
|
-
const interfaces =
|
|
2691
|
+
const interfaces = os2.networkInterfaces();
|
|
3002
2692
|
for (const addrs of Object.values(interfaces)) {
|
|
3003
2693
|
for (const addr of addrs ?? []) {
|
|
3004
2694
|
if (addr.family === "IPv4" && !addr.internal) {
|
|
@@ -3029,6 +2719,149 @@ var PipelineOrchestrator = class {
|
|
|
3029
2719
|
t("orchestrator.previewComment.expiry", { hours: ttlHours })
|
|
3030
2720
|
].join("\n");
|
|
3031
2721
|
}
|
|
2722
|
+
async resolveConflict(issueIid) {
|
|
2723
|
+
const record = this.tracker.get(issueIid);
|
|
2724
|
+
if (!record) throw new Error(`Issue ${issueIid} not found in tracker`);
|
|
2725
|
+
const baseBranch = this.config.project.baseBranch;
|
|
2726
|
+
const branchName = record.branchName;
|
|
2727
|
+
logger10.info("Starting conflict resolution", { issueIid, branchName, baseBranch });
|
|
2728
|
+
this.tracker.updateState(issueIid, "resolving_conflict" /* ResolvingConflict */);
|
|
2729
|
+
eventBus.emitTyped("conflict:started", { issueIid });
|
|
2730
|
+
try {
|
|
2731
|
+
await this.gongfeng.createIssueNote(
|
|
2732
|
+
record.issueId,
|
|
2733
|
+
t("conflict.startComment", { branch: branchName, baseBranch })
|
|
2734
|
+
);
|
|
2735
|
+
} catch {
|
|
2736
|
+
}
|
|
2737
|
+
const wtCtx = this.computeWorktreeContext(issueIid, branchName);
|
|
2738
|
+
try {
|
|
2739
|
+
await this.mainGitMutex.runExclusive(async () => {
|
|
2740
|
+
await this.mainGit.fetch();
|
|
2741
|
+
await this.ensureWorktree(wtCtx);
|
|
2742
|
+
});
|
|
2743
|
+
const wtGit = new GitOperations(wtCtx.gitRootDir);
|
|
2744
|
+
await wtGit.checkout(branchName);
|
|
2745
|
+
if (await wtGit.isRebaseInProgress()) {
|
|
2746
|
+
logger10.warn("Found residual rebase in progress, aborting", { issueIid });
|
|
2747
|
+
await wtGit.rebaseAbort();
|
|
2748
|
+
}
|
|
2749
|
+
const rebaseResult = await wtGit.rebase(`origin/${baseBranch}`);
|
|
2750
|
+
if (rebaseResult.success) {
|
|
2751
|
+
await wtGit.forcePush(branchName);
|
|
2752
|
+
this.tracker.updateState(issueIid, "completed" /* Completed */);
|
|
2753
|
+
eventBus.emitTyped("conflict:resolved", { issueIid });
|
|
2754
|
+
try {
|
|
2755
|
+
await this.gongfeng.createIssueNote(
|
|
2756
|
+
record.issueId,
|
|
2757
|
+
t("conflict.noConflictComment", { branch: branchName, baseBranch })
|
|
2758
|
+
);
|
|
2759
|
+
} catch {
|
|
2760
|
+
}
|
|
2761
|
+
await this.commentOnMr(record.mrUrl, t("conflict.mrResolvedComment"));
|
|
2762
|
+
logger10.info("Conflict resolution completed (no conflicts)", { issueIid });
|
|
2763
|
+
return;
|
|
2764
|
+
}
|
|
2765
|
+
let conflictFiles = rebaseResult.conflictFiles;
|
|
2766
|
+
const maxResolveAttempts = 20;
|
|
2767
|
+
let attempt = 0;
|
|
2768
|
+
while (conflictFiles.length > 0 && attempt < maxResolveAttempts) {
|
|
2769
|
+
attempt++;
|
|
2770
|
+
logger10.info("Resolving conflicts with AI", {
|
|
2771
|
+
issueIid,
|
|
2772
|
+
attempt,
|
|
2773
|
+
conflictFiles
|
|
2774
|
+
});
|
|
2775
|
+
const prompt = conflictResolvePrompt({
|
|
2776
|
+
issueIid,
|
|
2777
|
+
branchName,
|
|
2778
|
+
baseBranch,
|
|
2779
|
+
conflictFiles
|
|
2780
|
+
});
|
|
2781
|
+
await this.aiRunner.run({
|
|
2782
|
+
prompt,
|
|
2783
|
+
workDir: wtCtx.workDir,
|
|
2784
|
+
timeoutMs: this.config.ai.phaseTimeoutMs,
|
|
2785
|
+
onStreamEvent: (event) => {
|
|
2786
|
+
eventBus.emitTyped("agent:output", {
|
|
2787
|
+
issueIid,
|
|
2788
|
+
phase: "conflict-resolve",
|
|
2789
|
+
event
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
});
|
|
2793
|
+
await wtGit.add(conflictFiles);
|
|
2794
|
+
const continueResult = await wtGit.rebaseContinue();
|
|
2795
|
+
if (continueResult.done) {
|
|
2796
|
+
conflictFiles = [];
|
|
2797
|
+
} else {
|
|
2798
|
+
conflictFiles = continueResult.conflictFiles;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
if (conflictFiles.length > 0) {
|
|
2802
|
+
await wtGit.rebaseAbort();
|
|
2803
|
+
throw new Error(`Failed to resolve all conflicts after ${maxResolveAttempts} attempts. Remaining: ${conflictFiles.join(", ")}`);
|
|
2804
|
+
}
|
|
2805
|
+
logger10.info("Running verification after conflict resolution", { issueIid });
|
|
2806
|
+
const wtPlan = new PlanPersistence(wtCtx.workDir, issueIid);
|
|
2807
|
+
wtPlan.ensureDir();
|
|
2808
|
+
const verifyPhase = createPhase("verify", this.aiRunner, wtGit, wtPlan, this.gongfeng, this.tracker, this.config);
|
|
2809
|
+
const verifyCtx = {
|
|
2810
|
+
issueIid,
|
|
2811
|
+
issueId: record.issueId,
|
|
2812
|
+
issueTitle: record.issueTitle,
|
|
2813
|
+
issueDescription: "",
|
|
2814
|
+
branchName,
|
|
2815
|
+
pipelineMode: record.pipelineMode
|
|
2816
|
+
};
|
|
2817
|
+
await verifyPhase.execute(verifyCtx);
|
|
2818
|
+
await wtGit.forcePush(branchName);
|
|
2819
|
+
this.tracker.updateState(issueIid, "completed" /* Completed */);
|
|
2820
|
+
eventBus.emitTyped("conflict:resolved", { issueIid });
|
|
2821
|
+
try {
|
|
2822
|
+
await this.gongfeng.createIssueNote(
|
|
2823
|
+
record.issueId,
|
|
2824
|
+
t("conflict.resolvedComment", { branch: branchName, baseBranch })
|
|
2825
|
+
);
|
|
2826
|
+
} catch {
|
|
2827
|
+
}
|
|
2828
|
+
await this.commentOnMr(record.mrUrl, t("conflict.mrResolvedComment"));
|
|
2829
|
+
logger10.info("Conflict resolution completed", { issueIid });
|
|
2830
|
+
} catch (err) {
|
|
2831
|
+
const errorMsg = err.message;
|
|
2832
|
+
logger10.error("Conflict resolution failed", { issueIid, error: errorMsg });
|
|
2833
|
+
try {
|
|
2834
|
+
const wtGit = new GitOperations(wtCtx.gitRootDir);
|
|
2835
|
+
if (await wtGit.isRebaseInProgress()) {
|
|
2836
|
+
await wtGit.rebaseAbort();
|
|
2837
|
+
}
|
|
2838
|
+
} catch {
|
|
2839
|
+
}
|
|
2840
|
+
this.tracker.markFailed(issueIid, errorMsg.slice(0, 500), "resolving_conflict" /* ResolvingConflict */);
|
|
2841
|
+
eventBus.emitTyped("conflict:failed", { issueIid, error: errorMsg });
|
|
2842
|
+
try {
|
|
2843
|
+
await this.gongfeng.createIssueNote(
|
|
2844
|
+
record.issueId,
|
|
2845
|
+
t("conflict.failedComment", { error: errorMsg })
|
|
2846
|
+
);
|
|
2847
|
+
} catch {
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
extractMrIidFromUrl(mrUrl) {
|
|
2852
|
+
const match = mrUrl.match(/merge_requests\/(\d+)/);
|
|
2853
|
+
return match ? parseInt(match[1], 10) : null;
|
|
2854
|
+
}
|
|
2855
|
+
async commentOnMr(mrUrl, body) {
|
|
2856
|
+
if (!mrUrl) return;
|
|
2857
|
+
const mrIid = this.extractMrIidFromUrl(mrUrl);
|
|
2858
|
+
if (!mrIid) return;
|
|
2859
|
+
try {
|
|
2860
|
+
await this.gongfeng.createMergeRequestNote(mrIid, body);
|
|
2861
|
+
} catch (err) {
|
|
2862
|
+
logger10.warn("Failed to comment on MR", { mrIid, error: err.message });
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
3032
2865
|
};
|
|
3033
2866
|
|
|
3034
2867
|
// src/services/BrainstormService.ts
|
|
@@ -3102,7 +2935,7 @@ ${questions}
|
|
|
3102
2935
|
}
|
|
3103
2936
|
|
|
3104
2937
|
// src/services/BrainstormService.ts
|
|
3105
|
-
var
|
|
2938
|
+
var logger11 = logger.child("Brainstorm");
|
|
3106
2939
|
function agentConfigToAIConfig(agentCfg, timeoutMs) {
|
|
3107
2940
|
return {
|
|
3108
2941
|
mode: agentCfg.mode,
|
|
@@ -3138,7 +2971,7 @@ var BrainstormService = class {
|
|
|
3138
2971
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3139
2972
|
};
|
|
3140
2973
|
this.sessions.set(session.id, session);
|
|
3141
|
-
|
|
2974
|
+
logger11.info("Created brainstorm session", { sessionId: session.id });
|
|
3142
2975
|
return session;
|
|
3143
2976
|
}
|
|
3144
2977
|
getSession(id) {
|
|
@@ -3147,7 +2980,7 @@ var BrainstormService = class {
|
|
|
3147
2980
|
async generate(sessionId, onEvent) {
|
|
3148
2981
|
const session = this.requireSession(sessionId);
|
|
3149
2982
|
session.status = "generating";
|
|
3150
|
-
|
|
2983
|
+
logger11.info("Generating SDD", { sessionId });
|
|
3151
2984
|
const prompt = buildGeneratePrompt(session.transcript);
|
|
3152
2985
|
const result = await this.generatorRunner.run({
|
|
3153
2986
|
prompt,
|
|
@@ -3173,7 +3006,7 @@ var BrainstormService = class {
|
|
|
3173
3006
|
const session = this.requireSession(sessionId);
|
|
3174
3007
|
const roundNum = session.rounds.length + 1;
|
|
3175
3008
|
session.status = "reviewing";
|
|
3176
|
-
|
|
3009
|
+
logger11.info("Reviewing SDD", { sessionId, round: roundNum });
|
|
3177
3010
|
onEvent?.({ type: "round:start", data: { round: roundNum, phase: "review" }, round: roundNum });
|
|
3178
3011
|
const prompt = buildReviewPrompt(session.currentSdd, roundNum);
|
|
3179
3012
|
const result = await this.reviewerRunner.run({
|
|
@@ -3206,7 +3039,7 @@ var BrainstormService = class {
|
|
|
3206
3039
|
throw new Error("No review round to refine from");
|
|
3207
3040
|
}
|
|
3208
3041
|
session.status = "refining";
|
|
3209
|
-
|
|
3042
|
+
logger11.info("Refining SDD", { sessionId, round: currentRound.round });
|
|
3210
3043
|
const prompt = buildRefinePrompt(currentRound.questions);
|
|
3211
3044
|
const result = await this.generatorRunner.run({
|
|
3212
3045
|
prompt,
|
|
@@ -3262,17 +3095,9 @@ var BrainstormService = class {
|
|
|
3262
3095
|
};
|
|
3263
3096
|
|
|
3264
3097
|
export {
|
|
3265
|
-
loadConfig,
|
|
3266
|
-
Logger,
|
|
3267
|
-
logger,
|
|
3268
3098
|
AGENT_NOTE_MARKER,
|
|
3269
3099
|
GongfengClient,
|
|
3270
3100
|
GitOperations,
|
|
3271
|
-
BaseAIRunner,
|
|
3272
|
-
ClaudeInternalRunner,
|
|
3273
|
-
CodebuddyRunner,
|
|
3274
|
-
CursorAgentRunner,
|
|
3275
|
-
createAIRunner,
|
|
3276
3101
|
CLASSIC_PIPELINE,
|
|
3277
3102
|
PLAN_MODE_PIPELINE,
|
|
3278
3103
|
resolvePipelineMode,
|
|
@@ -3292,4 +3117,4 @@ export {
|
|
|
3292
3117
|
PipelineOrchestrator,
|
|
3293
3118
|
BrainstormService
|
|
3294
3119
|
};
|
|
3295
|
-
//# sourceMappingURL=chunk-
|
|
3120
|
+
//# sourceMappingURL=chunk-N5YK6YVI.js.map
|