@xdevops/issue-auto-finish 1.0.0

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.
Files changed (161) hide show
  1. package/bin/issue-auto-finish.js +2 -0
  2. package/dist/ai-runner/AIRunner.d.ts +27 -0
  3. package/dist/ai-runner/AIRunner.d.ts.map +1 -0
  4. package/dist/ai-runner/BaseAIRunner.d.ts +19 -0
  5. package/dist/ai-runner/BaseAIRunner.d.ts.map +1 -0
  6. package/dist/ai-runner/ClaudeInternalRunner.d.ts +13 -0
  7. package/dist/ai-runner/ClaudeInternalRunner.d.ts.map +1 -0
  8. package/dist/ai-runner/CodebuddyRunner.d.ts +13 -0
  9. package/dist/ai-runner/CodebuddyRunner.d.ts.map +1 -0
  10. package/dist/ai-runner/CursorAgentRunner.d.ts +13 -0
  11. package/dist/ai-runner/CursorAgentRunner.d.ts.map +1 -0
  12. package/dist/ai-runner/index.d.ts +15 -0
  13. package/dist/ai-runner/index.d.ts.map +1 -0
  14. package/dist/chunk-HCHEFK4Z.js +80 -0
  15. package/dist/chunk-HCHEFK4Z.js.map +1 -0
  16. package/dist/chunk-I3T573SU.js +153 -0
  17. package/dist/chunk-I3T573SU.js.map +1 -0
  18. package/dist/chunk-IDUKWCC2.js +1995 -0
  19. package/dist/chunk-IDUKWCC2.js.map +1 -0
  20. package/dist/chunk-OWVT3Z34.js +770 -0
  21. package/dist/chunk-OWVT3Z34.js.map +1 -0
  22. package/dist/chunk-RIUI4ROA.js +180 -0
  23. package/dist/chunk-RIUI4ROA.js.map +1 -0
  24. package/dist/chunk-TBIEB3JY.js +3295 -0
  25. package/dist/chunk-TBIEB3JY.js.map +1 -0
  26. package/dist/cli/commands/doctor.d.ts +2 -0
  27. package/dist/cli/commands/doctor.d.ts.map +1 -0
  28. package/dist/cli/commands/init.d.ts +6 -0
  29. package/dist/cli/commands/init.d.ts.map +1 -0
  30. package/dist/cli/commands/start.d.ts +5 -0
  31. package/dist/cli/commands/start.d.ts.map +1 -0
  32. package/dist/cli/index.d.ts +3 -0
  33. package/dist/cli/index.d.ts.map +1 -0
  34. package/dist/cli/setup/ConfigGenerator.d.ts +44 -0
  35. package/dist/cli/setup/ConfigGenerator.d.ts.map +1 -0
  36. package/dist/cli/setup/DependencyChecker.d.ts +21 -0
  37. package/dist/cli/setup/DependencyChecker.d.ts.map +1 -0
  38. package/dist/cli.js +73 -0
  39. package/dist/cli.js.map +1 -0
  40. package/dist/clients/GongfengClient.d.ts +86 -0
  41. package/dist/clients/GongfengClient.d.ts.map +1 -0
  42. package/dist/config.d.ts +92 -0
  43. package/dist/config.d.ts.map +1 -0
  44. package/dist/deploy/DevServerManager.d.ts +21 -0
  45. package/dist/deploy/DevServerManager.d.ts.map +1 -0
  46. package/dist/deploy/PortAllocator.d.ts +20 -0
  47. package/dist/deploy/PortAllocator.d.ts.map +1 -0
  48. package/dist/deploy/index.d.ts +3 -0
  49. package/dist/deploy/index.d.ts.map +1 -0
  50. package/dist/doctor-B26Q6JWI.js +33 -0
  51. package/dist/doctor-B26Q6JWI.js.map +1 -0
  52. package/dist/e2e/E2eSettings.d.ts +6 -0
  53. package/dist/e2e/E2eSettings.d.ts.map +1 -0
  54. package/dist/e2e/ScreenshotCollector.d.ts +11 -0
  55. package/dist/e2e/ScreenshotCollector.d.ts.map +1 -0
  56. package/dist/e2e/ScreenshotPublisher.d.ts +16 -0
  57. package/dist/e2e/ScreenshotPublisher.d.ts.map +1 -0
  58. package/dist/events/EventBus.d.ts +14 -0
  59. package/dist/events/EventBus.d.ts.map +1 -0
  60. package/dist/git/GitOperations.d.ts +30 -0
  61. package/dist/git/GitOperations.d.ts.map +1 -0
  62. package/dist/git/WorktreeContext.d.ts +9 -0
  63. package/dist/git/WorktreeContext.d.ts.map +1 -0
  64. package/dist/i18n/index.d.ts +8 -0
  65. package/dist/i18n/index.d.ts.map +1 -0
  66. package/dist/i18n/locales/en.d.ts +2 -0
  67. package/dist/i18n/locales/en.d.ts.map +1 -0
  68. package/dist/i18n/locales/zh-CN.d.ts +2 -0
  69. package/dist/i18n/locales/zh-CN.d.ts.map +1 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +12 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/init-L3VIWCOV.js +65 -0
  75. package/dist/init-L3VIWCOV.js.map +1 -0
  76. package/dist/lib.d.ts +26 -0
  77. package/dist/lib.d.ts.map +1 -0
  78. package/dist/lib.js +50 -0
  79. package/dist/lib.js.map +1 -0
  80. package/dist/logger.d.ts +15 -0
  81. package/dist/logger.d.ts.map +1 -0
  82. package/dist/notesync/NoteSyncSettings.d.ts +12 -0
  83. package/dist/notesync/NoteSyncSettings.d.ts.map +1 -0
  84. package/dist/orchestrator/PipelineOrchestrator.d.ts +53 -0
  85. package/dist/orchestrator/PipelineOrchestrator.d.ts.map +1 -0
  86. package/dist/persistence/PlanPersistence.d.ts +37 -0
  87. package/dist/persistence/PlanPersistence.d.ts.map +1 -0
  88. package/dist/phases/AnalysisPhase.d.ts +13 -0
  89. package/dist/phases/AnalysisPhase.d.ts.map +1 -0
  90. package/dist/phases/BasePhase.d.ts +44 -0
  91. package/dist/phases/BasePhase.d.ts.map +1 -0
  92. package/dist/phases/BuildPhase.d.ts +9 -0
  93. package/dist/phases/BuildPhase.d.ts.map +1 -0
  94. package/dist/phases/DesignPhase.d.ts +13 -0
  95. package/dist/phases/DesignPhase.d.ts.map +1 -0
  96. package/dist/phases/ImplementPhase.d.ts +9 -0
  97. package/dist/phases/ImplementPhase.d.ts.map +1 -0
  98. package/dist/phases/PhaseFactory.d.ts +13 -0
  99. package/dist/phases/PhaseFactory.d.ts.map +1 -0
  100. package/dist/phases/PlanPhase.d.ts +14 -0
  101. package/dist/phases/PlanPhase.d.ts.map +1 -0
  102. package/dist/phases/VerifyPhase.d.ts +13 -0
  103. package/dist/phases/VerifyPhase.d.ts.map +1 -0
  104. package/dist/pipeline/PipelineDefinition.d.ts +40 -0
  105. package/dist/pipeline/PipelineDefinition.d.ts.map +1 -0
  106. package/dist/poller/IssuePoller.d.ts +26 -0
  107. package/dist/poller/IssuePoller.d.ts.map +1 -0
  108. package/dist/prompts/brainstorm-templates.d.ts +4 -0
  109. package/dist/prompts/brainstorm-templates.d.ts.map +1 -0
  110. package/dist/prompts/templates.d.ts +27 -0
  111. package/dist/prompts/templates.d.ts.map +1 -0
  112. package/dist/rules/RuleResolver.d.ts +13 -0
  113. package/dist/rules/RuleResolver.d.ts.map +1 -0
  114. package/dist/rules/index.d.ts +3 -0
  115. package/dist/rules/index.d.ts.map +1 -0
  116. package/dist/run.d.ts +2 -0
  117. package/dist/run.d.ts.map +1 -0
  118. package/dist/run.js +15 -0
  119. package/dist/run.js.map +1 -0
  120. package/dist/services/BrainstormService.d.ts +39 -0
  121. package/dist/services/BrainstormService.d.ts.map +1 -0
  122. package/dist/start-TVN4SS6E.js +25 -0
  123. package/dist/start-TVN4SS6E.js.map +1 -0
  124. package/dist/supplement/SupplementStore.d.ts +21 -0
  125. package/dist/supplement/SupplementStore.d.ts.map +1 -0
  126. package/dist/tracker/IssueState.d.ts +63 -0
  127. package/dist/tracker/IssueState.d.ts.map +1 -0
  128. package/dist/tracker/IssueTracker.d.ts +29 -0
  129. package/dist/tracker/IssueTracker.d.ts.map +1 -0
  130. package/dist/utils/AsyncMutex.d.ts +14 -0
  131. package/dist/utils/AsyncMutex.d.ts.map +1 -0
  132. package/dist/utils/MergeRequestHelper.d.ts +10 -0
  133. package/dist/utils/MergeRequestHelper.d.ts.map +1 -0
  134. package/dist/web/AgentLogStore.d.ts +22 -0
  135. package/dist/web/AgentLogStore.d.ts.map +1 -0
  136. package/dist/web/WebServer.d.ts +26 -0
  137. package/dist/web/WebServer.d.ts.map +1 -0
  138. package/dist/web/routes/api.d.ts +20 -0
  139. package/dist/web/routes/api.d.ts.map +1 -0
  140. package/dist/web/routes/brainstorm.d.ts +9 -0
  141. package/dist/web/routes/brainstorm.d.ts.map +1 -0
  142. package/dist/web/routes/setup.d.ts +8 -0
  143. package/dist/web/routes/setup.d.ts.map +1 -0
  144. package/dist/webhook/CommandExecutor.d.ts +45 -0
  145. package/dist/webhook/CommandExecutor.d.ts.map +1 -0
  146. package/dist/webhook/CommandParser.d.ts +24 -0
  147. package/dist/webhook/CommandParser.d.ts.map +1 -0
  148. package/dist/webhook/IntentRecognizer.d.ts +35 -0
  149. package/dist/webhook/IntentRecognizer.d.ts.map +1 -0
  150. package/dist/webhook/NoteDeduplicator.d.ts +18 -0
  151. package/dist/webhook/NoteDeduplicator.d.ts.map +1 -0
  152. package/dist/webhook/WebhookHandler.d.ts +30 -0
  153. package/dist/webhook/WebhookHandler.d.ts.map +1 -0
  154. package/dist/webhook/WebhookServer.d.ts +21 -0
  155. package/dist/webhook/WebhookServer.d.ts.map +1 -0
  156. package/dist/webhook/index.d.ts +12 -0
  157. package/dist/webhook/index.d.ts.map +1 -0
  158. package/package.json +82 -0
  159. package/src/web/frontend/dist/assets/index-CQdlU9PE.js +65 -0
  160. package/src/web/frontend/dist/assets/index-CgMEkyZJ.css +1 -0
  161. package/src/web/frontend/dist/index.html +13 -0
@@ -0,0 +1,3295 @@
1
+ import {
2
+ t
3
+ } from "./chunk-OWVT3Z34.js";
4
+
5
+ // src/config.ts
6
+ import { config as loadDotenv } from "dotenv";
7
+ import path from "path";
8
+ import fs from "fs";
9
+ import os from "os";
10
+ import { fileURLToPath } from "url";
11
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ function resolveEnvPath() {
13
+ if (process.env.IAF_CONFIG_PATH) return process.env.IAF_CONFIG_PATH;
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();
214
+
215
+ // src/clients/GongfengClient.ts
216
+ import fs2 from "fs";
217
+ import path2 from "path";
218
+ var logger2 = logger.child("GongfengClient");
219
+ var AGENT_NOTE_MARKER = "\n\n<!-- issue-auto-finish-agent -->";
220
+ var AGENT_NOTE_MARKER_PATTERN = "<!-- issue-auto-finish-agent -->";
221
+ var GongfengClient = class {
222
+ apiUrl;
223
+ token;
224
+ projectPath;
225
+ constructor(config) {
226
+ this.apiUrl = config.apiUrl.replace(/\/$/, "");
227
+ this.token = config.privateToken;
228
+ this.projectPath = config.projectPath;
229
+ }
230
+ get projectApiBase() {
231
+ const encoded = encodeURIComponent(this.projectPath);
232
+ return `${this.apiUrl}/api/v3/projects/${encoded}`;
233
+ }
234
+ async requestRaw(path11, options = {}) {
235
+ const url = `${this.projectApiBase}${path11}`;
236
+ logger2.debug("API request", { method: options.method || "GET", url });
237
+ const resp = await fetch(url, {
238
+ ...options,
239
+ headers: {
240
+ "PRIVATE-TOKEN": this.token,
241
+ "Content-Type": "application/json",
242
+ ...options.headers
243
+ }
244
+ });
245
+ if (!resp.ok) {
246
+ const body = await resp.text();
247
+ throw new Error(`Gongfeng API error ${resp.status}: ${body}`);
248
+ }
249
+ return resp;
250
+ }
251
+ async request(path11, options = {}) {
252
+ const resp = await this.requestRaw(path11, options);
253
+ return resp.json();
254
+ }
255
+ async createIssue(title, description, labels) {
256
+ const body = { title, description };
257
+ if (labels && labels.length > 0) {
258
+ body.labels = labels.join(",");
259
+ }
260
+ const issue = await this.request("/issues", {
261
+ method: "POST",
262
+ body: JSON.stringify(body)
263
+ });
264
+ logger2.info("Issue created", { id: issue.id, iid: issue.iid, title });
265
+ return issue;
266
+ }
267
+ async listIssues(state = "opened", labels) {
268
+ const params = new URLSearchParams({ state, per_page: "100" });
269
+ if (labels) {
270
+ params.set("labels", labels);
271
+ }
272
+ return this.request(`/issues?${params.toString()}`);
273
+ }
274
+ async listIssuesAdvanced(options = {}) {
275
+ const params = new URLSearchParams({
276
+ state: options.state || "opened",
277
+ page: String(options.page || 1),
278
+ per_page: String(options.perPage || 20)
279
+ });
280
+ if (options.labels) {
281
+ params.set("labels", options.labels);
282
+ }
283
+ if (options.search) {
284
+ params.set("search", options.search);
285
+ }
286
+ const resp = await this.requestRaw(`/issues?${params.toString()}`);
287
+ const total = parseInt(resp.headers.get("x-total") || "0", 10);
288
+ const issues = await resp.json();
289
+ return { issues, total: total || issues.length };
290
+ }
291
+ async getIssueDetail(issueId) {
292
+ return this.request(`/issues/${issueId}`);
293
+ }
294
+ async createIssueNote(issueId, body) {
295
+ const markedBody = body + AGENT_NOTE_MARKER;
296
+ await this.request(`/issues/${issueId}/notes`, {
297
+ method: "POST",
298
+ body: JSON.stringify({ body: markedBody })
299
+ });
300
+ logger2.info("Issue note created", { issueId });
301
+ }
302
+ async updateIssueLabels(issueId, labels) {
303
+ await this.request(`/issues/${issueId}`, {
304
+ method: "PUT",
305
+ body: JSON.stringify({ labels: labels.join(",") })
306
+ });
307
+ logger2.info("Issue labels updated", { issueId, labels });
308
+ }
309
+ async createMergeRequest(options) {
310
+ const mr = await this.request("/merge_requests", {
311
+ method: "POST",
312
+ body: JSON.stringify({
313
+ source_branch: options.sourceBranch,
314
+ target_branch: options.targetBranch,
315
+ title: options.title,
316
+ description: options.description ?? ""
317
+ })
318
+ });
319
+ if (!mr.web_url && mr.iid) {
320
+ mr.web_url = this.buildMergeRequestUrl(mr.iid);
321
+ }
322
+ logger2.info("Merge request created", { iid: mr.iid, webUrl: mr.web_url });
323
+ return mr;
324
+ }
325
+ async findMergeRequestByBranch(sourceBranch, targetBranch, state = "opened") {
326
+ const params = new URLSearchParams({
327
+ state,
328
+ source_branch: sourceBranch,
329
+ target_branch: targetBranch
330
+ });
331
+ const mrs = await this.request(
332
+ `/merge_requests?${params.toString()}`
333
+ );
334
+ if (mrs.length === 0) return null;
335
+ const mr = mrs[0];
336
+ if (!mr.web_url && mr.iid) {
337
+ mr.web_url = this.buildMergeRequestUrl(mr.iid);
338
+ }
339
+ return mr;
340
+ }
341
+ buildMergeRequestUrl(mrIid) {
342
+ return `${this.apiUrl}/${this.projectPath}/merge_requests/${mrIid}`;
343
+ }
344
+ async uploadFile(filePath) {
345
+ const fileData = fs2.readFileSync(filePath);
346
+ const fileName = path2.basename(filePath);
347
+ const mimeType = fileName.endsWith(".png") ? "image/png" : "application/octet-stream";
348
+ const blob = new Blob([fileData], { type: mimeType });
349
+ const formData = new FormData();
350
+ formData.append("file", blob, fileName);
351
+ const url = `${this.projectApiBase}/uploads`;
352
+ logger2.debug("Upload request", { url, fileName });
353
+ const resp = await fetch(url, {
354
+ method: "POST",
355
+ headers: { "PRIVATE-TOKEN": this.token },
356
+ body: formData
357
+ });
358
+ if (!resp.ok) {
359
+ const body = await resp.text();
360
+ throw new Error(`Gongfeng upload error ${resp.status}: ${body}`);
361
+ }
362
+ const result = await resp.json();
363
+ logger2.info("File uploaded", { fileName, url: result.url });
364
+ return result;
365
+ }
366
+ async createMergeRequestNote(mrIid, body) {
367
+ const markedBody = body + AGENT_NOTE_MARKER;
368
+ await this.request(`/merge_requests/${mrIid}/notes`, {
369
+ method: "POST",
370
+ body: JSON.stringify({ body: markedBody })
371
+ });
372
+ logger2.info("Merge request note created", { mrIid });
373
+ }
374
+ async closeMergeRequest(mrIid) {
375
+ await this.request(`/merge_requests/${mrIid}`, {
376
+ method: "PUT",
377
+ body: JSON.stringify({ state_event: "close" })
378
+ });
379
+ logger2.info("Merge request closed", { mrIid });
380
+ }
381
+ async deleteIssue(issueId) {
382
+ await this.request(`/issues/${issueId}`, { method: "DELETE" });
383
+ logger2.info("Issue deleted", { issueId });
384
+ }
385
+ async closeIssue(issueId) {
386
+ await this.request(`/issues/${issueId}`, {
387
+ method: "PUT",
388
+ body: JSON.stringify({ state_event: "close", labels: "auto-finish:e2e-cleaned" })
389
+ });
390
+ logger2.info("Issue closed", { issueId });
391
+ }
392
+ async listIssueNotes(issueId) {
393
+ const allNotes = [];
394
+ let page = 1;
395
+ while (true) {
396
+ const params = new URLSearchParams({ per_page: "100", page: String(page) });
397
+ const batch = await this.request(
398
+ `/issues/${issueId}/notes?${params.toString()}`
399
+ );
400
+ allNotes.push(...batch);
401
+ if (batch.length < 100) break;
402
+ page++;
403
+ }
404
+ return allNotes;
405
+ }
406
+ async deleteIssueNote(issueId, noteId) {
407
+ await this.request(`/issues/${issueId}/notes/${noteId}`, { method: "DELETE" });
408
+ logger2.debug("Issue note deleted", { issueId, noteId });
409
+ }
410
+ async cleanupAgentNotes(issueId) {
411
+ const notes = await this.listIssueNotes(issueId);
412
+ const agentNotes = notes.filter((n) => n.body.includes(AGENT_NOTE_MARKER_PATTERN));
413
+ for (const note of agentNotes) {
414
+ await this.deleteIssueNote(issueId, note.id);
415
+ }
416
+ if (agentNotes.length > 0) {
417
+ logger2.info("Agent notes cleaned up", { issueId, deleted: agentNotes.length });
418
+ }
419
+ return agentNotes.length;
420
+ }
421
+ async addLabel(issueId, label) {
422
+ const issue = await this.getIssueDetail(issueId);
423
+ if (issue.labels.includes(label)) {
424
+ logger2.info("Label already exists, skipping", { issueId, label });
425
+ return;
426
+ }
427
+ const newLabels = [...issue.labels, label];
428
+ await this.updateIssueLabels(issueId, newLabels);
429
+ }
430
+ };
431
+
432
+ // src/git/GitOperations.ts
433
+ import { execFile } from "child_process";
434
+ import { promisify } from "util";
435
+ var execFileAsync = promisify(execFile);
436
+ var logger3 = logger.child("GitOperations");
437
+ var GitOperations = class {
438
+ workDir;
439
+ constructor(workDir) {
440
+ this.workDir = workDir;
441
+ }
442
+ async exec(args) {
443
+ logger3.debug("git exec", { args });
444
+ const { stdout } = await execFileAsync("git", args, {
445
+ cwd: this.workDir,
446
+ maxBuffer: 10 * 1024 * 1024,
447
+ env: { ...process.env, HUSKY: "0" }
448
+ });
449
+ return stdout.trim();
450
+ }
451
+ async fetchAndPull(branch) {
452
+ await this.exec(["fetch", "origin"]);
453
+ await this.exec(["checkout", "-f", branch]);
454
+ await this.exec(["pull", "origin", branch]);
455
+ logger3.info("Fetched and pulled", { branch });
456
+ }
457
+ async fetch() {
458
+ await this.exec(["fetch", "origin"]);
459
+ logger3.info("Fetched from origin");
460
+ }
461
+ async createBranch(name, from) {
462
+ await this.exec(["checkout", "-f", "-b", name, `origin/${from}`]);
463
+ logger3.info("Branch created", { name, from: `origin/${from}` });
464
+ }
465
+ async checkout(branch) {
466
+ await this.exec(["checkout", "-f", branch]);
467
+ logger3.info("Checked out", { branch });
468
+ }
469
+ async add(files) {
470
+ await this.exec(["add", ...files]);
471
+ }
472
+ async commit(message) {
473
+ await this.exec(["commit", "--no-verify", "-m", message]);
474
+ logger3.info("Committed", { message: message.slice(0, 80) });
475
+ }
476
+ async push(branch) {
477
+ await this.exec(["push", "--no-verify", "-u", "origin", branch]);
478
+ logger3.info("Pushed", { branch });
479
+ }
480
+ async branchExists(name) {
481
+ try {
482
+ await this.exec(["rev-parse", "--verify", name]);
483
+ return true;
484
+ } catch {
485
+ return false;
486
+ }
487
+ }
488
+ async remoteBranchExists(name) {
489
+ try {
490
+ await this.exec(["ls-remote", "--exit-code", "--heads", "origin", name]);
491
+ return true;
492
+ } catch {
493
+ return false;
494
+ }
495
+ }
496
+ async getCurrentBranch() {
497
+ return this.exec(["rev-parse", "--abbrev-ref", "HEAD"]);
498
+ }
499
+ async stash() {
500
+ await this.exec(["stash"]);
501
+ }
502
+ async stashPop() {
503
+ await this.exec(["stash", "pop"]);
504
+ }
505
+ async hasChanges() {
506
+ const status = await this.exec(["status", "--porcelain"]);
507
+ return status.length > 0;
508
+ }
509
+ async addAndCommit(files, message) {
510
+ await this.add(files);
511
+ await this.commit(message);
512
+ }
513
+ async checkoutTrack(remoteBranch) {
514
+ await this.exec(["checkout", "-f", "--track", `origin/${remoteBranch}`]);
515
+ logger3.info("Checked out remote tracking branch", { remoteBranch });
516
+ }
517
+ async addCommitAndPush(files, message, branch) {
518
+ await this.add(files);
519
+ await this.commit(message);
520
+ await this.push(branch);
521
+ }
522
+ async deleteBranch(name) {
523
+ await this.exec(["branch", "-D", name]);
524
+ logger3.info("Branch deleted", { name });
525
+ }
526
+ async deleteRemoteBranch(name) {
527
+ await this.exec(["push", "origin", "--delete", name]);
528
+ logger3.info("Remote branch deleted", { name });
529
+ }
530
+ async worktreeAdd(dir, newBranch, startPoint) {
531
+ await this.exec(["worktree", "add", "-b", newBranch, dir, startPoint]);
532
+ logger3.info("Worktree added (new branch)", { dir, newBranch, startPoint });
533
+ }
534
+ async worktreeAddExisting(dir, branch) {
535
+ await this.exec(["worktree", "add", dir, branch]);
536
+ logger3.info("Worktree added (existing branch)", { dir, branch });
537
+ }
538
+ async worktreeAddTracking(dir, remoteBranch) {
539
+ await this.exec(["worktree", "add", "--track", "-b", remoteBranch, dir, `origin/${remoteBranch}`]);
540
+ logger3.info("Worktree added (tracking remote)", { dir, remoteBranch });
541
+ }
542
+ async worktreeRemove(dir, force = false) {
543
+ const args = ["worktree", "remove", dir];
544
+ if (force) args.push("--force");
545
+ await this.exec(args);
546
+ logger3.info("Worktree removed", { dir, force });
547
+ }
548
+ async worktreeList() {
549
+ const output = await this.exec(["worktree", "list", "--porcelain"]);
550
+ return output.split("\n").filter((line) => line.startsWith("worktree ")).map((line) => line.replace("worktree ", ""));
551
+ }
552
+ async showFile(ref, filePath) {
553
+ try {
554
+ return await this.exec(["show", `${ref}:${filePath}`]);
555
+ } catch {
556
+ return null;
557
+ }
558
+ }
559
+ };
560
+
561
+ // src/ai-runner/BaseAIRunner.ts
562
+ import { spawn } from "child_process";
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
+ });
648
+ }
649
+ emitStreamLine(line, onStreamEvent) {
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
+ }
681
+ try {
682
+ const parsed = JSON.parse(rawOutput);
683
+ if (Array.isArray(parsed)) {
684
+ output = parsed.filter((item) => item.type === "result").map((item) => item.result ?? "").join("\n");
685
+ const sessionItem = parsed.find(
686
+ (item) => item.session_id != null
687
+ );
688
+ if (sessionItem) {
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;
726
+ }
727
+ }
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
+ }
761
+ getSpawnOptions(options) {
762
+ const { CLAUDECODE, ...env } = process.env;
763
+ return {
764
+ cwd: options.workDir,
765
+ env: {
766
+ ...env,
767
+ NODE_VERSION: this.nvmNodeVersion
768
+ }
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);
799
+ }
800
+ return args;
801
+ }
802
+ getSpawnOptions(options) {
803
+ return {
804
+ cwd: options.workDir,
805
+ env: {
806
+ ...process.env,
807
+ NODE_VERSION: this.nvmNodeVersion
808
+ }
809
+ };
810
+ }
811
+ };
812
+
813
+ // src/ai-runner/CursorAgentRunner.ts
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;
848
+ }
849
+ getSpawnOptions(_options) {
850
+ return {
851
+ cwd: process.cwd(),
852
+ env: {
853
+ ...process.env,
854
+ NODE_VERSION: this.nvmNodeVersion
855
+ }
856
+ };
857
+ }
858
+ };
859
+
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
+ // src/pipeline/PipelineDefinition.ts
873
+ var CLASSIC_PIPELINE = {
874
+ mode: "classic",
875
+ phases: [
876
+ {
877
+ name: "analysis",
878
+ label: "\u5206\u6790",
879
+ startState: "analyzing" /* Analyzing */,
880
+ doneState: "analysis_done" /* AnalysisDone */,
881
+ kind: "ai"
882
+ },
883
+ {
884
+ name: "design",
885
+ label: "\u8BBE\u8BA1",
886
+ startState: "designing" /* Designing */,
887
+ doneState: "design_done" /* DesignDone */,
888
+ kind: "ai"
889
+ },
890
+ {
891
+ name: "implement",
892
+ label: "\u5B9E\u65BD",
893
+ startState: "implementing" /* Implementing */,
894
+ doneState: "implement_done" /* ImplementDone */,
895
+ kind: "ai"
896
+ },
897
+ {
898
+ name: "verify",
899
+ label: "\u9A8C\u8BC1",
900
+ startState: "verifying" /* Verifying */,
901
+ doneState: "completed" /* Completed */,
902
+ kind: "ai"
903
+ }
904
+ ],
905
+ planFiles: [
906
+ { filename: "01-analysis.md", label: "\u9700\u6C42\u5206\u6790", editable: true },
907
+ { filename: "02-design.md", label: "\u7CFB\u7EDF\u8BBE\u8BA1", editable: true },
908
+ { filename: "03-todolist.md", label: "\u5B9E\u65BD\u6E05\u5355", editable: true },
909
+ { filename: "04-verify-report.md", label: "\u9A8C\u8BC1\u62A5\u544A", editable: false }
910
+ ]
911
+ };
912
+ var PLAN_MODE_PIPELINE = {
913
+ mode: "plan-mode",
914
+ phases: [
915
+ {
916
+ name: "plan",
917
+ label: "\u89C4\u5212",
918
+ startState: "planning" /* Planning */,
919
+ doneState: "plan_done" /* PlanDone */,
920
+ kind: "ai"
921
+ },
922
+ {
923
+ name: "review",
924
+ label: "\u5BA1\u6838",
925
+ startState: "waiting_for_review" /* WaitingForReview */,
926
+ doneState: "waiting_for_review" /* WaitingForReview */,
927
+ approvedState: "review_approved" /* ReviewApproved */,
928
+ kind: "gate"
929
+ },
930
+ {
931
+ name: "build",
932
+ label: "\u5B9E\u65BD",
933
+ startState: "building" /* Building */,
934
+ doneState: "build_done" /* BuildDone */,
935
+ kind: "ai"
936
+ },
937
+ {
938
+ name: "verify",
939
+ label: "\u9A8C\u8BC1",
940
+ startState: "verifying" /* Verifying */,
941
+ doneState: "completed" /* Completed */,
942
+ kind: "ai"
943
+ }
944
+ ],
945
+ planFiles: [
946
+ { filename: "01-plan.md", label: "\u5B9E\u65BD\u8BA1\u5212", editable: true },
947
+ { filename: "02-verify-report.md", label: "\u9A8C\u8BC1\u62A5\u544A", editable: false },
948
+ { filename: "review-feedback.md", label: "\u5BA1\u6838\u53CD\u9988", editable: false },
949
+ { filename: "review-history.json", label: "\u5BA1\u6838\u5386\u53F2", editable: false }
950
+ ]
951
+ };
952
+ function resolvePipelineMode(aiMode, explicit) {
953
+ if (explicit === "classic" || explicit === "plan-mode") return explicit;
954
+ return aiMode === "cursor-agent" ? "plan-mode" : "classic";
955
+ }
956
+ function getPipelineDef(mode) {
957
+ return mode === "plan-mode" ? PLAN_MODE_PIPELINE : CLASSIC_PIPELINE;
958
+ }
959
+ function getPhasePreState(def, phaseName) {
960
+ const idx = def.phases.findIndex((p) => p.name === phaseName);
961
+ if (idx < 0) return void 0;
962
+ if (idx === 0) return "branch_created" /* BranchCreated */;
963
+ const prev = def.phases[idx - 1];
964
+ return prev.approvedState ?? prev.doneState;
965
+ }
966
+ function collectStateLabels(def) {
967
+ const labels = /* @__PURE__ */ new Map();
968
+ labels.set("pending" /* Pending */, t("state.pending"));
969
+ labels.set("branch_created" /* BranchCreated */, t("state.branchCreated"));
970
+ for (const phase of def.phases) {
971
+ labels.set(phase.startState, t("state.phaseDoing", { label: t(`pipeline.phase.${phase.name}`) }));
972
+ if (phase.doneState !== "completed" /* Completed */) {
973
+ labels.set(phase.doneState, t("state.phaseDone", { label: t(`pipeline.phase.${phase.name}`) }));
974
+ }
975
+ if (phase.approvedState) labels.set(phase.approvedState, t("state.phaseApproved", { label: t(`pipeline.phase.${phase.name}`) }));
976
+ }
977
+ labels.set("completed" /* Completed */, t("state.completed"));
978
+ labels.set("failed" /* Failed */, t("state.failed"));
979
+ return labels;
980
+ }
981
+
982
+ // src/events/EventBus.ts
983
+ import { EventEmitter } from "events";
984
+ var EventBusImpl = class extends EventEmitter {
985
+ emit(event, ...args) {
986
+ super.emit("*", event, ...args);
987
+ return super.emit(event, ...args);
988
+ }
989
+ emitTyped(type, data) {
990
+ const payload = {
991
+ type,
992
+ data,
993
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
994
+ };
995
+ this.emit(type, payload);
996
+ }
997
+ };
998
+ var eventBus = new EventBusImpl();
999
+
1000
+ // src/tracker/IssueTracker.ts
1001
+ import fs3 from "fs";
1002
+ import path3 from "path";
1003
+ var logger5 = logger.child("IssueTracker");
1004
+ var IssueTracker = class _IssueTracker {
1005
+ filePath;
1006
+ data;
1007
+ constructor(dataDir) {
1008
+ this.filePath = path3.join(dataDir, "tracker.json");
1009
+ this.data = this.load();
1010
+ }
1011
+ load() {
1012
+ try {
1013
+ if (fs3.existsSync(this.filePath)) {
1014
+ const raw = fs3.readFileSync(this.filePath, "utf-8");
1015
+ return JSON.parse(raw);
1016
+ }
1017
+ } catch (err) {
1018
+ logger5.error("Failed to load tracker data", { error: err.message });
1019
+ }
1020
+ return { issues: {} };
1021
+ }
1022
+ save() {
1023
+ const dir = path3.dirname(this.filePath);
1024
+ if (!fs3.existsSync(dir)) {
1025
+ fs3.mkdirSync(dir, { recursive: true });
1026
+ }
1027
+ const tmpPath = path3.join(dir, `.tracker-${process.pid}-${Date.now()}.tmp`);
1028
+ fs3.writeFileSync(tmpPath, JSON.stringify(this.data, null, 2), "utf-8");
1029
+ fs3.renameSync(tmpPath, this.filePath);
1030
+ }
1031
+ key(issueIid) {
1032
+ return String(issueIid);
1033
+ }
1034
+ get(issueIid) {
1035
+ return this.data.issues[this.key(issueIid)];
1036
+ }
1037
+ create(record) {
1038
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1039
+ const full = {
1040
+ ...record,
1041
+ attempts: 0,
1042
+ createdAt: now,
1043
+ updatedAt: now
1044
+ };
1045
+ this.data.issues[this.key(record.issueIid)] = full;
1046
+ this.save();
1047
+ logger5.info("Issue tracked", { issueIid: record.issueIid, state: record.state });
1048
+ eventBus.emitTyped("issue:created", full);
1049
+ return full;
1050
+ }
1051
+ updateState(issueIid, state, extra) {
1052
+ const record = this.data.issues[this.key(issueIid)];
1053
+ if (!record) {
1054
+ throw new Error(`Issue ${issueIid} not found in tracker`);
1055
+ }
1056
+ record.state = state;
1057
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1058
+ if (state === "completed" /* Completed */) {
1059
+ record.lastError = void 0;
1060
+ record.failedAtState = void 0;
1061
+ }
1062
+ if (extra) {
1063
+ Object.assign(record, extra);
1064
+ }
1065
+ this.save();
1066
+ logger5.info("Issue state updated", { issueIid, state });
1067
+ eventBus.emitTyped("issue:stateChanged", { issueIid, state, record });
1068
+ }
1069
+ markFailed(issueIid, error, failedAtState) {
1070
+ const record = this.data.issues[this.key(issueIid)];
1071
+ if (!record) return;
1072
+ record.state = "failed" /* Failed */;
1073
+ record.lastError = error;
1074
+ record.failedAtState = failedAtState;
1075
+ record.attempts += 1;
1076
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1077
+ this.save();
1078
+ logger5.warn("Issue marked as failed", { issueIid, error, failedAtState, attempts: record.attempts });
1079
+ eventBus.emitTyped("issue:failed", { issueIid, error, failedAtState, record });
1080
+ }
1081
+ static TERMINAL_STATES = /* @__PURE__ */ new Set(["completed" /* Completed */, "failed" /* Failed */]);
1082
+ static PHASE_DONE_STATES = /* @__PURE__ */ new Set([
1083
+ "analysis_done" /* AnalysisDone */,
1084
+ "design_done" /* DesignDone */,
1085
+ "implement_done" /* ImplementDone */,
1086
+ "plan_done" /* PlanDone */,
1087
+ "build_done" /* BuildDone */
1088
+ ]);
1089
+ isProcessing(issueIid) {
1090
+ const record = this.get(issueIid);
1091
+ if (!record) return false;
1092
+ return !_IssueTracker.TERMINAL_STATES.has(record.state);
1093
+ }
1094
+ isCompleted(issueIid) {
1095
+ const record = this.get(issueIid);
1096
+ return record?.state === "completed" /* Completed */;
1097
+ }
1098
+ canRetry(issueIid, maxRetries) {
1099
+ const record = this.get(issueIid);
1100
+ if (!record || record.state !== "failed" /* Failed */) return false;
1101
+ return record.attempts < maxRetries;
1102
+ }
1103
+ getRetryState(issueIid) {
1104
+ const record = this.get(issueIid);
1105
+ return record?.failedAtState;
1106
+ }
1107
+ isStalled(issueIid, thresholdMs = 5 * 60 * 1e3) {
1108
+ const record = this.get(issueIid);
1109
+ if (!record) return false;
1110
+ if (record.state === "waiting_for_review" /* WaitingForReview */) return false;
1111
+ if (!this.isProcessing(issueIid)) return false;
1112
+ const elapsed = Date.now() - new Date(record.updatedAt).getTime();
1113
+ return elapsed > thresholdMs;
1114
+ }
1115
+ getDrivableIssues(maxRetries, stalledThresholdMs) {
1116
+ return Object.values(this.data.issues).filter((record) => {
1117
+ if (record.state === "waiting_for_review" /* WaitingForReview */) return false;
1118
+ if (record.state === "pending" /* Pending */) return true;
1119
+ if (record.state === "branch_created" /* BranchCreated */) return true;
1120
+ if (record.state === "review_approved" /* ReviewApproved */) return true;
1121
+ if (record.state === "failed" /* Failed */ && record.attempts < maxRetries) return true;
1122
+ if (_IssueTracker.PHASE_DONE_STATES.has(record.state)) return true;
1123
+ if (this.isStalled(record.issueIid, stalledThresholdMs)) return true;
1124
+ return false;
1125
+ });
1126
+ }
1127
+ getAllActive() {
1128
+ return Object.values(this.data.issues).filter(
1129
+ (r) => r.state !== "completed" /* Completed */ && r.state !== "failed" /* Failed */
1130
+ );
1131
+ }
1132
+ getAll() {
1133
+ return Object.values(this.data.issues);
1134
+ }
1135
+ resetFull(issueIid) {
1136
+ const record = this.data.issues[this.key(issueIid)];
1137
+ if (!record) return false;
1138
+ record.state = "pending" /* Pending */;
1139
+ record.attempts = 0;
1140
+ record.sessionId = void 0;
1141
+ record.failedAtState = void 0;
1142
+ record.lastError = void 0;
1143
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1144
+ this.save();
1145
+ logger5.info("Issue fully reset", { issueIid });
1146
+ eventBus.emitTyped("issue:restarted", { issueIid, record });
1147
+ return true;
1148
+ }
1149
+ resetToPhase(issueIid, phase, def) {
1150
+ const record = this.data.issues[this.key(issueIid)];
1151
+ if (!record) return false;
1152
+ const targetState = getPhasePreState(def, phase);
1153
+ if (!targetState) return false;
1154
+ record.state = targetState;
1155
+ record.sessionId = void 0;
1156
+ record.failedAtState = void 0;
1157
+ record.lastError = void 0;
1158
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1159
+ this.save();
1160
+ logger5.info("Issue reset to phase", { issueIid, phase, state: targetState });
1161
+ eventBus.emitTyped("issue:retryFromPhase", { issueIid, phase, record });
1162
+ return true;
1163
+ }
1164
+ resetForRetry(issueIid) {
1165
+ const record = this.data.issues[this.key(issueIid)];
1166
+ if (!record || record.state !== "failed" /* Failed */) return false;
1167
+ const restoreState = record.failedAtState ?? "pending" /* Pending */;
1168
+ record.state = restoreState;
1169
+ record.lastError = void 0;
1170
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1171
+ this.save();
1172
+ logger5.info("Issue reset for retry", { issueIid, restoreState });
1173
+ eventBus.emitTyped("issue:resetForRetry", { issueIid, restoreState, record });
1174
+ return true;
1175
+ }
1176
+ delete(issueIid) {
1177
+ const key = this.key(issueIid);
1178
+ if (!this.data.issues[key]) return false;
1179
+ const record = this.data.issues[key];
1180
+ delete this.data.issues[key];
1181
+ this.save();
1182
+ logger5.info("Issue deleted from tracker", { issueIid });
1183
+ eventBus.emitTyped("issue:deleted", { issueIid, record });
1184
+ return true;
1185
+ }
1186
+ };
1187
+
1188
+ // src/persistence/PlanPersistence.ts
1189
+ import fs4 from "fs";
1190
+ import path4 from "path";
1191
+ var logger6 = logger.child("PlanPersistence");
1192
+ var PLAN_DIR = ".claude-plan";
1193
+ var PlanPersistence = class _PlanPersistence {
1194
+ workDir;
1195
+ issueIid;
1196
+ constructor(workDir, issueIid) {
1197
+ this.workDir = workDir;
1198
+ this.issueIid = issueIid;
1199
+ }
1200
+ get baseDir() {
1201
+ return this.workDir;
1202
+ }
1203
+ get planDir() {
1204
+ return path4.join(this.workDir, PLAN_DIR, `issue-${this.issueIid}`);
1205
+ }
1206
+ ensureDir() {
1207
+ if (!fs4.existsSync(this.planDir)) {
1208
+ fs4.mkdirSync(this.planDir, { recursive: true });
1209
+ }
1210
+ }
1211
+ writeIssueMeta(meta) {
1212
+ this.ensureDir();
1213
+ const filePath = path4.join(this.planDir, "issue-meta.json");
1214
+ fs4.writeFileSync(filePath, JSON.stringify(meta, null, 2), "utf-8");
1215
+ logger6.info("Issue meta written");
1216
+ }
1217
+ writeProgress(data) {
1218
+ this.ensureDir();
1219
+ const filePath = path4.join(this.planDir, "progress.json");
1220
+ fs4.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
1221
+ logger6.debug("Progress written", { currentPhase: data.currentPhase });
1222
+ }
1223
+ readProgress() {
1224
+ const filePath = path4.join(this.planDir, "progress.json");
1225
+ if (!fs4.existsSync(filePath)) return null;
1226
+ try {
1227
+ return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
1228
+ } catch {
1229
+ return null;
1230
+ }
1231
+ }
1232
+ writeAnalysis(content) {
1233
+ this.ensureDir();
1234
+ fs4.writeFileSync(path4.join(this.planDir, "01-analysis.md"), content, "utf-8");
1235
+ logger6.info("Analysis document written");
1236
+ }
1237
+ writeDesign(content) {
1238
+ this.ensureDir();
1239
+ fs4.writeFileSync(path4.join(this.planDir, "02-design.md"), content, "utf-8");
1240
+ logger6.info("Design document written");
1241
+ }
1242
+ writeTodolist(content) {
1243
+ this.ensureDir();
1244
+ fs4.writeFileSync(path4.join(this.planDir, "03-todolist.md"), content, "utf-8");
1245
+ logger6.info("Todolist written");
1246
+ }
1247
+ writeVerifyReport(content, filename = "04-verify-report.md") {
1248
+ this.ensureDir();
1249
+ fs4.writeFileSync(path4.join(this.planDir, filename), content, "utf-8");
1250
+ logger6.info("Verify report written", { filename });
1251
+ }
1252
+ getAllPlanFiles() {
1253
+ if (!fs4.existsSync(this.planDir)) return [];
1254
+ return fs4.readdirSync(this.planDir).map((f) => path4.join(PLAN_DIR, `issue-${this.issueIid}`, f));
1255
+ }
1256
+ createInitialProgress(issueId, issueTitle, branchName, def) {
1257
+ const pending = { status: "pending" };
1258
+ if (def) {
1259
+ const phases = {};
1260
+ for (const spec of def.phases) {
1261
+ phases[spec.name] = { ...pending };
1262
+ }
1263
+ return {
1264
+ issueId,
1265
+ issueTitle,
1266
+ branchName,
1267
+ pipelineMode: def.mode,
1268
+ currentPhase: def.phases[0].name,
1269
+ phases
1270
+ };
1271
+ }
1272
+ return {
1273
+ issueId,
1274
+ issueTitle,
1275
+ branchName,
1276
+ currentPhase: "analysis",
1277
+ phases: {
1278
+ analysis: { ...pending },
1279
+ design: { ...pending },
1280
+ implement: { ...pending },
1281
+ verify: { ...pending }
1282
+ }
1283
+ };
1284
+ }
1285
+ writePlan(content) {
1286
+ this.ensureDir();
1287
+ fs4.writeFileSync(path4.join(this.planDir, "01-plan.md"), content, "utf-8");
1288
+ logger6.info("Plan document written");
1289
+ }
1290
+ writeReviewFeedback(content) {
1291
+ this.ensureDir();
1292
+ const history = this.readReviewHistory();
1293
+ const round = {
1294
+ round: history.length + 1,
1295
+ feedback: content,
1296
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1297
+ };
1298
+ history.push(round);
1299
+ fs4.writeFileSync(
1300
+ path4.join(this.planDir, "review-history.json"),
1301
+ JSON.stringify(history, null, 2),
1302
+ "utf-8"
1303
+ );
1304
+ fs4.writeFileSync(
1305
+ path4.join(this.planDir, "review-feedback.md"),
1306
+ _PlanPersistence.renderReviewHistoryMarkdown(history),
1307
+ "utf-8"
1308
+ );
1309
+ logger6.info("Review feedback appended", { round: round.round });
1310
+ }
1311
+ readReviewFeedback() {
1312
+ const filePath = path4.join(this.planDir, "review-feedback.md");
1313
+ if (!fs4.existsSync(filePath)) return null;
1314
+ try {
1315
+ return fs4.readFileSync(filePath, "utf-8");
1316
+ } catch {
1317
+ return null;
1318
+ }
1319
+ }
1320
+ readReviewHistory() {
1321
+ const filePath = path4.join(this.planDir, "review-history.json");
1322
+ if (!fs4.existsSync(filePath)) return [];
1323
+ try {
1324
+ const data = JSON.parse(fs4.readFileSync(filePath, "utf-8"));
1325
+ return Array.isArray(data) ? data : [];
1326
+ } catch {
1327
+ return [];
1328
+ }
1329
+ }
1330
+ static renderReviewHistoryMarkdown(history) {
1331
+ if (history.length === 0) return "";
1332
+ const lines = ["# \u5BA1\u6838\u53CD\u9988\u5386\u53F2", ""];
1333
+ for (const r of history) {
1334
+ lines.push(`## \u7B2C ${r.round} \u8F6E\u5BA1\u6838\u53CD\u9988`);
1335
+ lines.push(`> \u65F6\u95F4: ${r.timestamp}`);
1336
+ lines.push("");
1337
+ lines.push(r.feedback);
1338
+ lines.push("");
1339
+ }
1340
+ return lines.join("\n");
1341
+ }
1342
+ updatePhaseProgress(phaseName, status, error) {
1343
+ const progress = this.readProgress();
1344
+ if (!progress) return;
1345
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1346
+ if (!progress.phases[phaseName]) {
1347
+ progress.phases[phaseName] = { status: "pending" };
1348
+ }
1349
+ const phase = progress.phases[phaseName];
1350
+ phase.status = status;
1351
+ if (status === "in_progress") {
1352
+ phase.startedAt = now;
1353
+ progress.currentPhase = phaseName;
1354
+ } else if (status === "completed") {
1355
+ phase.completedAt = now;
1356
+ } else if (status === "failed") {
1357
+ phase.error = error;
1358
+ }
1359
+ this.writeProgress(progress);
1360
+ }
1361
+ };
1362
+
1363
+ // src/phases/BasePhase.ts
1364
+ import fs5 from "fs";
1365
+ import path6 from "path";
1366
+
1367
+ // src/prompts/templates.ts
1368
+ function planDir(iid) {
1369
+ return `.claude-plan/issue-${iid}`;
1370
+ }
1371
+ function analysisPrompt(ctx) {
1372
+ const supplementSection = ctx.supplementText ? `
1373
+
1374
+ ${ctx.supplementText}` : "";
1375
+ const pd = planDir(ctx.issueIid);
1376
+ return t("prompt.analysis", {
1377
+ iid: ctx.issueIid,
1378
+ title: ctx.issueTitle,
1379
+ description: ctx.issueDescription,
1380
+ supplement: supplementSection,
1381
+ planDir: pd
1382
+ });
1383
+ }
1384
+ function designPrompt(ctx) {
1385
+ const supplementSection = ctx.supplementText ? `
1386
+
1387
+ ${ctx.supplementText}` : "";
1388
+ const pd = planDir(ctx.issueIid);
1389
+ return t("prompt.design", {
1390
+ iid: ctx.issueIid,
1391
+ title: ctx.issueTitle,
1392
+ supplement: supplementSection,
1393
+ planDir: pd
1394
+ });
1395
+ }
1396
+ function implementPrompt(ctx) {
1397
+ const pd = planDir(ctx.issueIid);
1398
+ return t("prompt.implement", {
1399
+ iid: ctx.issueIid,
1400
+ title: ctx.issueTitle,
1401
+ planDir: pd
1402
+ });
1403
+ }
1404
+ function verifyPrompt(ctx) {
1405
+ const pd = planDir(ctx.issueIid);
1406
+ return t("prompt.verify", {
1407
+ iid: ctx.issueIid,
1408
+ title: ctx.issueTitle,
1409
+ planDir: pd
1410
+ });
1411
+ }
1412
+ function planModeVerifyPrompt(ctx) {
1413
+ const pd = planDir(ctx.issueIid);
1414
+ return t("prompt.planModeVerify", {
1415
+ iid: ctx.issueIid,
1416
+ title: ctx.issueTitle,
1417
+ planDir: pd
1418
+ });
1419
+ }
1420
+ function planPrompt(ctx) {
1421
+ const supplementSection = ctx.supplementText ? `
1422
+
1423
+ ${ctx.supplementText}` : "";
1424
+ const pd = planDir(ctx.issueIid);
1425
+ return t("prompt.plan", {
1426
+ iid: ctx.issueIid,
1427
+ title: ctx.issueTitle,
1428
+ description: ctx.issueDescription,
1429
+ supplement: supplementSection,
1430
+ planDir: pd
1431
+ });
1432
+ }
1433
+ function buildPrompt(ctx) {
1434
+ const pd = planDir(ctx.issueIid);
1435
+ return t("prompt.build", {
1436
+ iid: ctx.issueIid,
1437
+ title: ctx.issueTitle,
1438
+ planDir: pd
1439
+ });
1440
+ }
1441
+ function rePlanPrompt(ctx, history) {
1442
+ const supplementSection = ctx.supplementText ? `
1443
+
1444
+ ${ctx.supplementText}` : "";
1445
+ const pd = planDir(ctx.issueIid);
1446
+ const feedbackLines = history.map(
1447
+ (r) => t("prompt.rePlanRound", { round: r.round, timestamp: r.timestamp, feedback: r.feedback })
1448
+ ).join("\n\n");
1449
+ return t("prompt.rePlan", {
1450
+ iid: ctx.issueIid,
1451
+ title: ctx.issueTitle,
1452
+ description: ctx.issueDescription,
1453
+ supplement: supplementSection,
1454
+ historyCount: history.length,
1455
+ feedbackLines,
1456
+ planDir: pd
1457
+ });
1458
+ }
1459
+ function e2eVerifyPromptSuffix(ctx, ports) {
1460
+ const serverSection = ports ? `
1461
+ **Preview \u73AF\u5883\u5DF2\u542F\u52A8\uFF08\u7531\u7CFB\u7EDF\u7BA1\u7406\uFF0C\u65E0\u9700\u624B\u52A8\u542F\u52A8\uFF09\uFF1A**
1462
+ - \u540E\u7AEF: http://${ports.host}:${ports.backendPort}
1463
+ - \u524D\u7AEF: https://${ports.host}:${ports.frontendPort}
1464
+
1465
+ \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
+ \`\`\`bash
1467
+ E2E_PORT=${ports.frontendPort} E2E_HOST=${ports.host} E2E_BASE_URL=https://${ports.host}:${ports.frontendPort} \\
1468
+ cd frontend && npx playwright test
1469
+ \`\`\`
1470
+
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 playwright test \u5373\u53EF\u590D\u7528\u5DF2\u542F\u52A8\u7684\u524D\u7AEF\u3002` : `
1472
+ \u6267\u884C E2E \u6D4B\u8BD5\uFF1A
1473
+ \`\`\`bash
1474
+ cd frontend && pnpm test:e2e
1475
+ \`\`\``;
1476
+ return `
1477
+
1478
+ ## E2E UI \u9A8C\u8BC1\uFF08\u5DF2\u542F\u7528\uFF09
1479
+
1480
+ \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
+
1482
+ 6. \u5982\u679C\u672C\u6B21\u53D8\u66F4\u6D89\u53CA\u524D\u7AEF\u9875\u9762\uFF08frontend/ \u76EE\u5F55\u6709\u6539\u52A8\uFF09\uFF0C\u8BF7\u6267\u884C UI E2E \u9A8C\u8BC1\uFF1A
1483
+ a. \u5728 frontend/e2e/dynamic/ \u76EE\u5F55\u4E0B\u7F16\u5199\u9488\u5BF9\u672C\u6B21\u53D8\u66F4\u7684 Playwright \u6D4B\u8BD5
1484
+ b. ${serverSection.trim()}
1485
+ c. \u5982\u679C\u6D4B\u8BD5\u5931\u8D25\uFF0C\u5206\u6790\u5931\u8D25\u539F\u56E0\u5E76\u5C1D\u8BD5\u4FEE\u590D
1486
+ 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
1487
+ - \u5192\u70DF\u6D4B\u8BD5\u901A\u8FC7\u6570 / \u603B\u6570
1488
+ - \u4E13\u9879\u6D4B\u8BD5\u7ED3\u679C\u5217\u8868
1489
+ - \u5931\u8D25\u622A\u56FE\u8DEF\u5F84\uFF08\u5982\u6709\uFF09`;
1490
+ }
1491
+ function issueProgressComment(phase, status, detail) {
1492
+ const emoji = {
1493
+ analysis: "\u{1F50D}",
1494
+ design: "\u{1F4D0}",
1495
+ implement: "\u{1F4BB}",
1496
+ verify: "\u2705",
1497
+ plan: "\u{1F4CB}",
1498
+ review: "\u{1F440}",
1499
+ build: "\u{1F528}"
1500
+ };
1501
+ const icon = emoji[phase] || "\u{1F4CB}";
1502
+ const statusKey = status === "completed" ? "progress.completed" : status === "failed" ? "progress.failed" : "progress.inProgress";
1503
+ const statusText = t(statusKey);
1504
+ let msg = t("progress.comment", { icon, phase, status: statusText });
1505
+ if (detail) {
1506
+ msg += `
1507
+
1508
+ ${detail}`;
1509
+ }
1510
+ return msg;
1511
+ }
1512
+
1513
+ // src/rules/RuleResolver.ts
1514
+ import { readdir, readFile } from "fs/promises";
1515
+ import path5 from "path";
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
+ ];
1546
+ function parseFrontmatter(raw) {
1547
+ const fmRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
1548
+ const match = raw.match(fmRegex);
1549
+ if (!match) {
1550
+ return { description: "", content: raw.trim() };
1551
+ }
1552
+ const yamlBlock = match[1];
1553
+ const content = raw.slice(match[0].length).trim();
1554
+ let description = "";
1555
+ const descMatch = yamlBlock.match(/description:\s*(.+)/);
1556
+ if (descMatch) {
1557
+ description = descMatch[1].trim();
1558
+ }
1559
+ return { description, content };
1560
+ }
1561
+ var RuleResolver = class {
1562
+ rules = [];
1563
+ async loadRules(rulesDir) {
1564
+ this.rules = [];
1565
+ let files;
1566
+ try {
1567
+ files = await readdir(rulesDir);
1568
+ } catch {
1569
+ return;
1570
+ }
1571
+ const mdcFiles = files.filter((f) => f.endsWith(".mdc"));
1572
+ const loadPromises = mdcFiles.map(async (filename) => {
1573
+ try {
1574
+ const raw = await readFile(path5.join(rulesDir, filename), "utf-8");
1575
+ const { description, content } = parseFrontmatter(raw);
1576
+ if (content) {
1577
+ this.rules.push({ filename, description, content });
1578
+ }
1579
+ } catch {
1580
+ }
1581
+ });
1582
+ await Promise.all(loadPromises);
1583
+ }
1584
+ getRules() {
1585
+ return this.rules;
1586
+ }
1587
+ matchRules(text) {
1588
+ const lowerText = text.toLowerCase();
1589
+ const matched = [];
1590
+ for (const trigger of RULE_TRIGGERS) {
1591
+ const isTriggered = trigger.keywords.some((kw) => lowerText.includes(kw.toLowerCase()));
1592
+ if (!isTriggered) continue;
1593
+ const rule = this.rules.find((r) => r.filename === trigger.filename);
1594
+ if (rule) {
1595
+ matched.push(rule);
1596
+ }
1597
+ }
1598
+ return matched;
1599
+ }
1600
+ formatForPrompt(rules) {
1601
+ if (rules.length === 0) return "";
1602
+ return rules.map((rule) => {
1603
+ const header = rule.description ? `### ${rule.description} (${rule.filename})` : `### ${rule.filename}`;
1604
+ return `${header}
1605
+
1606
+ ${rule.content}`;
1607
+ }).join("\n\n---\n\n");
1608
+ }
1609
+ };
1610
+
1611
+ // src/notesync/NoteSyncSettings.ts
1612
+ var noteSyncOverride;
1613
+ function getNoteSyncEnabled(cfg) {
1614
+ return noteSyncOverride ?? cfg.issueNoteSync.enabled;
1615
+ }
1616
+ function setNoteSyncOverride(value) {
1617
+ noteSyncOverride = value;
1618
+ }
1619
+ function isNoteSyncEnabledForIssue(issueIid, tracker, cfg) {
1620
+ const record = tracker.get(issueIid);
1621
+ if (record?.issueNoteSyncEnabled !== void 0) return record.issueNoteSyncEnabled;
1622
+ return getNoteSyncEnabled(cfg);
1623
+ }
1624
+ var SUMMARY_MAX_LENGTH = 500;
1625
+ function truncateToSummary(content) {
1626
+ if (content.length <= SUMMARY_MAX_LENGTH) return content;
1627
+ const cut = content.slice(0, SUMMARY_MAX_LENGTH);
1628
+ const lastNewline = cut.lastIndexOf("\n\n");
1629
+ const boundary = lastNewline > SUMMARY_MAX_LENGTH * 0.3 ? lastNewline : cut.lastIndexOf("\n");
1630
+ const summary = boundary > SUMMARY_MAX_LENGTH * 0.3 ? cut.slice(0, boundary) : cut;
1631
+ return summary + "\n\n...";
1632
+ }
1633
+ function buildNoteSyncComment(phaseName, phaseLabel, docUrl, dashboardUrl, summary) {
1634
+ const emoji = {
1635
+ analysis: "\u{1F50D}",
1636
+ design: "\u{1F4D0}",
1637
+ implement: "\u{1F4BB}",
1638
+ verify: "\u2705",
1639
+ plan: "\u{1F4CB}",
1640
+ review: "\u{1F440}",
1641
+ build: "\u{1F528}"
1642
+ };
1643
+ const icon = emoji[phaseName] || "\u{1F4CB}";
1644
+ return [
1645
+ t("notesync.phaseCompleted", { icon, label: phaseLabel }),
1646
+ "",
1647
+ summary,
1648
+ "",
1649
+ "---",
1650
+ t("notesync.viewDoc", { label: phaseLabel, url: docUrl }),
1651
+ t("notesync.viewDashboard", { url: dashboardUrl })
1652
+ ].join("\n");
1653
+ }
1654
+
1655
+ // src/phases/BasePhase.ts
1656
+ var BasePhase = class {
1657
+ aiRunner;
1658
+ git;
1659
+ plan;
1660
+ gongfeng;
1661
+ tracker;
1662
+ config;
1663
+ logger;
1664
+ constructor(aiRunner, git, plan, gongfeng, tracker, config) {
1665
+ this.aiRunner = aiRunner;
1666
+ this.git = git;
1667
+ this.plan = plan;
1668
+ this.gongfeng = gongfeng;
1669
+ this.tracker = tracker;
1670
+ this.config = config;
1671
+ this.logger = logger.child(this.constructor.name);
1672
+ }
1673
+ getRunMode() {
1674
+ return void 0;
1675
+ }
1676
+ getResultFiles(_ctx) {
1677
+ return [];
1678
+ }
1679
+ async execute(ctx) {
1680
+ this.logger.info(`Phase ${this.phaseName} starting`, { issueIid: ctx.issueIid });
1681
+ this.tracker.updateState(ctx.issueIid, this.startState);
1682
+ this.plan.updatePhaseProgress(this.phaseName, "in_progress");
1683
+ try {
1684
+ await this.gongfeng.createIssueNote(
1685
+ ctx.issueId,
1686
+ issueProgressComment(this.phaseName, "in_progress")
1687
+ );
1688
+ } catch (err) {
1689
+ this.logger.warn("Failed to comment on issue", { error: err.message });
1690
+ }
1691
+ const phaseLabel = t(`phase.${this.phaseName}`) || this.phaseName;
1692
+ eventBus.emitTyped("agent:output", {
1693
+ issueIid: ctx.issueIid,
1694
+ phase: this.phaseName,
1695
+ event: {
1696
+ type: "system",
1697
+ content: t("basePhase.aiStarting", { label: phaseLabel }),
1698
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1699
+ }
1700
+ });
1701
+ const basePrompt = this.buildPrompt(ctx);
1702
+ const matchedRulesText = await this.resolveRules(ctx);
1703
+ const prompt = matchedRulesText ? `${basePrompt}
1704
+
1705
+ ${t("basePhase.rulesSection", { rules: matchedRulesText })}` : basePrompt;
1706
+ const record = this.tracker.get(ctx.issueIid);
1707
+ const result = await this.aiRunner.run({
1708
+ prompt,
1709
+ workDir: this.plan.baseDir,
1710
+ timeoutMs: this.config.ai.phaseTimeoutMs,
1711
+ sessionId: record?.sessionId,
1712
+ continueSession: !!record?.sessionId,
1713
+ mode: this.getRunMode(),
1714
+ onStreamEvent: (event) => {
1715
+ eventBus.emitTyped("agent:output", {
1716
+ issueIid: ctx.issueIid,
1717
+ phase: this.phaseName,
1718
+ event
1719
+ });
1720
+ }
1721
+ });
1722
+ if (!result.success) {
1723
+ this.plan.updatePhaseProgress(this.phaseName, "failed", result.output.slice(0, 500));
1724
+ this.tracker.markFailed(ctx.issueIid, result.output.slice(0, 500), this.startState);
1725
+ try {
1726
+ await this.gongfeng.createIssueNote(
1727
+ ctx.issueId,
1728
+ issueProgressComment(this.phaseName, "failed", t("basePhase.error", { message: result.output.slice(0, 200) }))
1729
+ );
1730
+ } catch {
1731
+ }
1732
+ throw new Error(`Phase ${this.phaseName} failed: ${result.output.slice(0, 200)}`);
1733
+ }
1734
+ if (result.sessionId) {
1735
+ this.tracker.updateState(ctx.issueIid, this.startState, { sessionId: result.sessionId });
1736
+ }
1737
+ this.tracker.updateState(ctx.issueIid, this.doneState);
1738
+ this.plan.updatePhaseProgress(this.phaseName, "completed");
1739
+ await this.commitPlanFiles(ctx);
1740
+ await this.syncResultToIssue(ctx);
1741
+ this.logger.info(`Phase ${this.phaseName} completed`, { issueIid: ctx.issueIid });
1742
+ return result;
1743
+ }
1744
+ async resolveRules(ctx) {
1745
+ try {
1746
+ const rulesDir = path6.join(this.plan.baseDir, ".cursor", "rules");
1747
+ const resolver = new RuleResolver();
1748
+ await resolver.loadRules(rulesDir);
1749
+ const context = `${ctx.issueTitle} ${ctx.issueDescription} ${ctx.supplementText ?? ""}`;
1750
+ const matched = resolver.matchRules(context);
1751
+ if (matched.length > 0) {
1752
+ this.logger.info(`Matched ${matched.length} MDC rules`, {
1753
+ rules: matched.map((r) => r.filename)
1754
+ });
1755
+ return resolver.formatForPrompt(matched);
1756
+ }
1757
+ } catch (err) {
1758
+ this.logger.warn("Failed to resolve MDC rules", { error: err.message });
1759
+ }
1760
+ return null;
1761
+ }
1762
+ async syncResultToIssue(ctx) {
1763
+ try {
1764
+ const enabled = isNoteSyncEnabledForIssue(ctx.issueIid, this.tracker, this.config);
1765
+ const resultFiles = this.getResultFiles(ctx);
1766
+ if (!enabled || resultFiles.length === 0) {
1767
+ try {
1768
+ await this.gongfeng.createIssueNote(
1769
+ ctx.issueId,
1770
+ issueProgressComment(this.phaseName, "completed")
1771
+ );
1772
+ } catch {
1773
+ }
1774
+ return;
1775
+ }
1776
+ const baseUrl = this.config.issueNoteSync.webBaseUrl.replace(/\/$/, "");
1777
+ const phaseLabel = t(`phase.${this.phaseName}`) || this.phaseName;
1778
+ const dashboardUrl = `${baseUrl}/?issue=${ctx.issueIid}`;
1779
+ for (const file of resultFiles) {
1780
+ const content = this.readResultFile(ctx.issueIid, file.filename);
1781
+ if (!content) continue;
1782
+ const summary = truncateToSummary(content);
1783
+ const docUrl = `${baseUrl}/doc/${ctx.issueIid}/${file.filename}`;
1784
+ const comment = buildNoteSyncComment(
1785
+ this.phaseName,
1786
+ file.label || phaseLabel,
1787
+ docUrl,
1788
+ dashboardUrl,
1789
+ summary
1790
+ );
1791
+ await this.gongfeng.createIssueNote(ctx.issueId, comment);
1792
+ this.logger.info("Result synced to issue", { issueIid: ctx.issueIid, file: file.filename });
1793
+ }
1794
+ } catch (err) {
1795
+ this.logger.warn("Failed to sync result to issue", { error: err.message });
1796
+ try {
1797
+ await this.gongfeng.createIssueNote(
1798
+ ctx.issueId,
1799
+ issueProgressComment(this.phaseName, "completed")
1800
+ );
1801
+ } catch {
1802
+ }
1803
+ }
1804
+ }
1805
+ readResultFile(issueIid, filename) {
1806
+ const planDir2 = path6.join(this.plan.baseDir, ".claude-plan", `issue-${issueIid}`);
1807
+ const filePath = path6.join(planDir2, filename);
1808
+ if (!fs5.existsSync(filePath)) return null;
1809
+ try {
1810
+ return fs5.readFileSync(filePath, "utf-8");
1811
+ } catch {
1812
+ return null;
1813
+ }
1814
+ }
1815
+ async commitPlanFiles(ctx) {
1816
+ if (await this.git.hasChanges()) {
1817
+ await this.git.add(["."]);
1818
+ await this.git.commit(`chore(auto): ${this.phaseName} phase completed for issue #${ctx.issueIid}`);
1819
+ await this.git.push(ctx.branchName);
1820
+ }
1821
+ }
1822
+ };
1823
+
1824
+ // src/phases/AnalysisPhase.ts
1825
+ var AnalysisPhase = class extends BasePhase {
1826
+ phaseName = "analysis";
1827
+ startState = "analyzing" /* Analyzing */;
1828
+ doneState = "analysis_done" /* AnalysisDone */;
1829
+ getResultFiles() {
1830
+ return [{ filename: "01-analysis.md", label: "\u9700\u6C42\u5206\u6790" }];
1831
+ }
1832
+ buildPrompt(ctx) {
1833
+ return analysisPrompt({
1834
+ issueTitle: ctx.issueTitle,
1835
+ issueDescription: ctx.issueDescription,
1836
+ issueIid: ctx.issueIid,
1837
+ supplementText: ctx.supplementText
1838
+ });
1839
+ }
1840
+ };
1841
+
1842
+ // src/phases/DesignPhase.ts
1843
+ var DesignPhase = class extends BasePhase {
1844
+ phaseName = "design";
1845
+ startState = "designing" /* Designing */;
1846
+ doneState = "design_done" /* DesignDone */;
1847
+ getResultFiles() {
1848
+ return [{ filename: "02-design.md", label: "\u7CFB\u7EDF\u8BBE\u8BA1" }];
1849
+ }
1850
+ buildPrompt(ctx) {
1851
+ return designPrompt({
1852
+ issueTitle: ctx.issueTitle,
1853
+ issueDescription: ctx.issueDescription,
1854
+ issueIid: ctx.issueIid,
1855
+ supplementText: ctx.supplementText
1856
+ });
1857
+ }
1858
+ };
1859
+
1860
+ // src/phases/ImplementPhase.ts
1861
+ var ImplementPhase = class extends BasePhase {
1862
+ phaseName = "implement";
1863
+ startState = "implementing" /* Implementing */;
1864
+ doneState = "implement_done" /* ImplementDone */;
1865
+ buildPrompt(ctx) {
1866
+ return implementPrompt({
1867
+ issueTitle: ctx.issueTitle,
1868
+ issueDescription: ctx.issueDescription,
1869
+ issueIid: ctx.issueIid
1870
+ });
1871
+ }
1872
+ };
1873
+
1874
+ // src/phases/VerifyPhase.ts
1875
+ import os2 from "os";
1876
+
1877
+ // src/e2e/E2eSettings.ts
1878
+ var e2eOverride;
1879
+ function getE2eEnabled(cfg) {
1880
+ return e2eOverride ?? cfg.e2e.enabled;
1881
+ }
1882
+ function setE2eOverride(value) {
1883
+ e2eOverride = value;
1884
+ }
1885
+ function isE2eEnabledForIssue(issueIid, tracker, cfg) {
1886
+ const record = tracker.get(issueIid);
1887
+ if (record?.e2eEnabled !== void 0) return record.e2eEnabled;
1888
+ return getE2eEnabled(cfg);
1889
+ }
1890
+
1891
+ // src/phases/VerifyPhase.ts
1892
+ function getDefaultHost() {
1893
+ const interfaces = os2.networkInterfaces();
1894
+ for (const addrs of Object.values(interfaces)) {
1895
+ for (const addr of addrs ?? []) {
1896
+ if (addr.family === "IPv4" && !addr.internal) return addr.address;
1897
+ }
1898
+ }
1899
+ return "localhost";
1900
+ }
1901
+ var VerifyPhase = class extends BasePhase {
1902
+ phaseName = "verify";
1903
+ startState = "verifying" /* Verifying */;
1904
+ doneState = "completed" /* Completed */;
1905
+ getResultFiles(ctx) {
1906
+ const filename = ctx?.pipelineMode === "plan-mode" ? "02-verify-report.md" : "04-verify-report.md";
1907
+ return [{ filename, label: "\u9A8C\u8BC1\u62A5\u544A" }];
1908
+ }
1909
+ buildPrompt(ctx) {
1910
+ const promptCtx = {
1911
+ issueTitle: ctx.issueTitle,
1912
+ issueDescription: ctx.issueDescription,
1913
+ issueIid: ctx.issueIid
1914
+ };
1915
+ const base = ctx.pipelineMode === "plan-mode" ? planModeVerifyPrompt(promptCtx) : verifyPrompt(promptCtx);
1916
+ if (!isE2eEnabledForIssue(ctx.issueIid, this.tracker, this.config)) {
1917
+ return base;
1918
+ }
1919
+ const e2ePorts = ctx.ports ? {
1920
+ backendPort: ctx.ports.backendPort,
1921
+ frontendPort: ctx.ports.frontendPort,
1922
+ host: this.config.preview.host || getDefaultHost()
1923
+ } : void 0;
1924
+ return base + e2eVerifyPromptSuffix(promptCtx, e2ePorts);
1925
+ }
1926
+ };
1927
+
1928
+ // src/phases/PlanPhase.ts
1929
+ var PlanPhase = class extends BasePhase {
1930
+ phaseName = "plan";
1931
+ startState = "planning" /* Planning */;
1932
+ doneState = "plan_done" /* PlanDone */;
1933
+ getResultFiles() {
1934
+ return [{ filename: "01-plan.md", label: "\u5B9E\u65BD\u8BA1\u5212" }];
1935
+ }
1936
+ getRunMode() {
1937
+ return "plan";
1938
+ }
1939
+ buildPrompt(ctx) {
1940
+ const history = this.plan.readReviewHistory();
1941
+ const promptCtx = {
1942
+ issueTitle: ctx.issueTitle,
1943
+ issueDescription: ctx.issueDescription,
1944
+ issueIid: ctx.issueIid,
1945
+ supplementText: ctx.supplementText
1946
+ };
1947
+ if (history.length > 0) {
1948
+ return rePlanPrompt(promptCtx, history);
1949
+ }
1950
+ return planPrompt(promptCtx);
1951
+ }
1952
+ };
1953
+
1954
+ // src/phases/BuildPhase.ts
1955
+ var BuildPhase = class extends BasePhase {
1956
+ phaseName = "build";
1957
+ startState = "building" /* Building */;
1958
+ doneState = "build_done" /* BuildDone */;
1959
+ buildPrompt(ctx) {
1960
+ return buildPrompt({
1961
+ issueTitle: ctx.issueTitle,
1962
+ issueDescription: ctx.issueDescription,
1963
+ issueIid: ctx.issueIid
1964
+ });
1965
+ }
1966
+ };
1967
+
1968
+ // src/phases/PhaseFactory.ts
1969
+ var PHASE_REGISTRY = {
1970
+ analysis: AnalysisPhase,
1971
+ design: DesignPhase,
1972
+ implement: ImplementPhase,
1973
+ plan: PlanPhase,
1974
+ build: BuildPhase,
1975
+ verify: VerifyPhase
1976
+ };
1977
+ function createPhase(name, ...args) {
1978
+ const Ctor = PHASE_REGISTRY[name];
1979
+ if (!Ctor) {
1980
+ throw new Error(
1981
+ `Unknown phase: ${name}. Registered phases: ${Object.keys(PHASE_REGISTRY).join(", ")}`
1982
+ );
1983
+ }
1984
+ return new Ctor(...args);
1985
+ }
1986
+ function validatePhaseRegistry(phaseNames) {
1987
+ const missing = phaseNames.filter((name) => !(name in PHASE_REGISTRY));
1988
+ if (missing.length > 0) {
1989
+ throw new Error(
1990
+ `Pipeline defines unregistered phases: ${missing.join(", ")}. Registered: ${Object.keys(PHASE_REGISTRY).join(", ")}`
1991
+ );
1992
+ }
1993
+ }
1994
+
1995
+ // src/utils/AsyncMutex.ts
1996
+ var AsyncMutex = class {
1997
+ queue = [];
1998
+ locked = false;
1999
+ async runExclusive(fn) {
2000
+ await this.acquire();
2001
+ try {
2002
+ return await fn();
2003
+ } finally {
2004
+ this.release();
2005
+ }
2006
+ }
2007
+ acquire() {
2008
+ if (!this.locked) {
2009
+ this.locked = true;
2010
+ return Promise.resolve();
2011
+ }
2012
+ return new Promise((resolve) => {
2013
+ this.queue.push(resolve);
2014
+ });
2015
+ }
2016
+ release() {
2017
+ const next = this.queue.shift();
2018
+ if (next) {
2019
+ next();
2020
+ } else {
2021
+ this.locked = false;
2022
+ }
2023
+ }
2024
+ get isLocked() {
2025
+ return this.locked;
2026
+ }
2027
+ get queueLength() {
2028
+ return this.queue.length;
2029
+ }
2030
+ };
2031
+
2032
+ // src/orchestrator/PipelineOrchestrator.ts
2033
+ import path10 from "path";
2034
+ import os3 from "os";
2035
+ import fs8 from "fs/promises";
2036
+ import { execFile as execFile2 } from "child_process";
2037
+ import { promisify as promisify2 } from "util";
2038
+
2039
+ // src/utils/MergeRequestHelper.ts
2040
+ import fs6 from "fs";
2041
+ import path7 from "path";
2042
+ var TAPD_PATTERNS = [
2043
+ /--story=(\d+)/i,
2044
+ /--bug=(\d+)/i,
2045
+ /tapd[:\s-]+(\d+)/i,
2046
+ /tapd单号[:\s]*(\d+)/i
2047
+ ];
2048
+ function extractTapdId(text) {
2049
+ for (const pattern of TAPD_PATTERNS) {
2050
+ const match = text.match(pattern);
2051
+ if (match) return match[1];
2052
+ }
2053
+ return null;
2054
+ }
2055
+ function generateMRTitle(issueIid, issueTitle, tapdId) {
2056
+ const base = `feat(#${issueIid}): ${issueTitle}`;
2057
+ if (tapdId) {
2058
+ return `${base} --story=${tapdId}`;
2059
+ }
2060
+ return base;
2061
+ }
2062
+ function generateMRDescription(options) {
2063
+ const { issueIid, issueTitle, issueDescription, branchName, planDir: planDir2 } = options;
2064
+ const sections = [
2065
+ t("mr.relatedIssue"),
2066
+ ``,
2067
+ `- Issue: #${issueIid}`,
2068
+ `- ${t("mr.title")}: ${issueTitle}`,
2069
+ `- ${t("mr.branch")}: \`${branchName}\``,
2070
+ ``,
2071
+ t("mr.issueDescription"),
2072
+ ``,
2073
+ issueDescription || t("mr.noDescription")
2074
+ ];
2075
+ if (planDir2) {
2076
+ const summaryFiles = [
2077
+ { filename: "01-analysis.md", label: t("mr.summaryFiles.01-analysis.md") },
2078
+ { filename: "01-plan.md", label: t("mr.summaryFiles.01-plan.md") },
2079
+ { filename: "02-design.md", label: t("mr.summaryFiles.02-design.md") },
2080
+ { filename: "04-verify-report.md", label: t("mr.summaryFiles.04-verify-report.md") },
2081
+ { filename: "02-verify-report.md", label: t("mr.summaryFiles.02-verify-report.md") }
2082
+ ];
2083
+ const planSections = [];
2084
+ for (const { filename, label } of summaryFiles) {
2085
+ const filePath = path7.join(planDir2, ".claude-plan", `issue-${issueIid}`, filename);
2086
+ if (fs6.existsSync(filePath)) {
2087
+ const content = fs6.readFileSync(filePath, "utf-8");
2088
+ const summary = extractSummary(content);
2089
+ if (summary) {
2090
+ planSections.push(`### ${label}
2091
+
2092
+ ${summary}`);
2093
+ }
2094
+ }
2095
+ }
2096
+ if (planSections.length > 0) {
2097
+ sections.push("", t("mr.aiSummary"), "", ...planSections);
2098
+ }
2099
+ }
2100
+ sections.push("", "---", t("mr.autoCreated"));
2101
+ return sections.join("\n");
2102
+ }
2103
+ function extractSummary(content, maxLines = 20) {
2104
+ const lines = content.split("\n");
2105
+ if (lines.length <= maxLines) return content.trim();
2106
+ return lines.slice(0, maxLines).join("\n").trim() + "\n\n" + t("mr.truncated");
2107
+ }
2108
+
2109
+ // src/deploy/PortAllocator.ts
2110
+ import net from "net";
2111
+ var logger7 = logger.child("PortAllocator");
2112
+ var DEFAULT_OPTIONS = {
2113
+ backendPortBase: 4e3,
2114
+ frontendPortBase: 9e3,
2115
+ maxPorts: 100
2116
+ };
2117
+ function checkPortAvailable(port) {
2118
+ return new Promise((resolve) => {
2119
+ const server = net.createServer();
2120
+ server.once("error", () => resolve(false));
2121
+ server.once("listening", () => {
2122
+ server.close(() => resolve(true));
2123
+ });
2124
+ server.listen(port, "0.0.0.0");
2125
+ });
2126
+ }
2127
+ var PortAllocator = class {
2128
+ allocated = /* @__PURE__ */ new Map();
2129
+ options;
2130
+ constructor(options) {
2131
+ this.options = { ...DEFAULT_OPTIONS, ...options };
2132
+ }
2133
+ async allocate(issueIid) {
2134
+ const existing = this.allocated.get(issueIid);
2135
+ if (existing) {
2136
+ logger7.info("Returning already allocated ports", { issueIid, ports: existing });
2137
+ return existing;
2138
+ }
2139
+ const usedBackend = new Set([...this.allocated.values()].map((p) => p.backendPort));
2140
+ const usedFrontend = new Set([...this.allocated.values()].map((p) => p.frontendPort));
2141
+ for (let offset = 1; offset <= this.options.maxPorts; offset++) {
2142
+ const backendPort = this.options.backendPortBase + offset;
2143
+ const frontendPort = this.options.frontendPortBase + offset;
2144
+ if (usedBackend.has(backendPort) || usedFrontend.has(frontendPort)) {
2145
+ continue;
2146
+ }
2147
+ const [beOk, feOk] = await Promise.all([
2148
+ checkPortAvailable(backendPort),
2149
+ checkPortAvailable(frontendPort)
2150
+ ]);
2151
+ if (beOk && feOk) {
2152
+ const pair = { backendPort, frontendPort };
2153
+ this.allocated.set(issueIid, pair);
2154
+ logger7.info("Ports allocated", { issueIid, ...pair });
2155
+ return pair;
2156
+ }
2157
+ logger7.debug("Port pair unavailable, trying next", {
2158
+ backendPort,
2159
+ frontendPort,
2160
+ beOk,
2161
+ feOk
2162
+ });
2163
+ }
2164
+ throw new Error(
2165
+ `No available port pair found for issue #${issueIid} (scanned ${this.options.maxPorts} offsets from backend=${this.options.backendPortBase} frontend=${this.options.frontendPortBase})`
2166
+ );
2167
+ }
2168
+ release(issueIid) {
2169
+ const pair = this.allocated.get(issueIid);
2170
+ if (pair) {
2171
+ this.allocated.delete(issueIid);
2172
+ logger7.info("Ports released", { issueIid, ...pair });
2173
+ }
2174
+ }
2175
+ getPortsForIssue(issueIid) {
2176
+ return this.allocated.get(issueIid);
2177
+ }
2178
+ getAllAllocated() {
2179
+ return new Map(this.allocated);
2180
+ }
2181
+ restore(issueIid, ports) {
2182
+ this.allocated.set(issueIid, ports);
2183
+ logger7.info("Ports restored from persistence", { issueIid, ...ports });
2184
+ }
2185
+ };
2186
+
2187
+ // src/deploy/DevServerManager.ts
2188
+ import { spawn as spawn2 } from "child_process";
2189
+ import path8 from "path";
2190
+ import https from "https";
2191
+ import http from "http";
2192
+ var logger8 = logger.child("DevServerManager");
2193
+ var DEFAULT_OPTIONS2 = {
2194
+ healthCheckTimeoutMs: 12e4,
2195
+ healthCheckIntervalMs: 3e3
2196
+ };
2197
+ function waitForPort(port, useTls, timeoutMs, intervalMs) {
2198
+ return new Promise((resolve, reject) => {
2199
+ const deadline = Date.now() + timeoutMs;
2200
+ const check = () => {
2201
+ if (Date.now() > deadline) {
2202
+ reject(new Error(`Port ${port} not ready after ${timeoutMs}ms`));
2203
+ return;
2204
+ }
2205
+ const req = useTls ? https.get(
2206
+ { hostname: "127.0.0.1", port, path: "/", rejectUnauthorized: false, timeout: 5e3 },
2207
+ (res) => {
2208
+ res.resume();
2209
+ resolve();
2210
+ }
2211
+ ) : http.get(
2212
+ { hostname: "127.0.0.1", port, path: "/", timeout: 5e3 },
2213
+ (res) => {
2214
+ res.resume();
2215
+ resolve();
2216
+ }
2217
+ );
2218
+ req.on("error", () => {
2219
+ setTimeout(check, intervalMs);
2220
+ });
2221
+ req.on("timeout", () => {
2222
+ req.destroy();
2223
+ setTimeout(check, intervalMs);
2224
+ });
2225
+ };
2226
+ check();
2227
+ });
2228
+ }
2229
+ var DevServerManager = class {
2230
+ servers = /* @__PURE__ */ new Map();
2231
+ options;
2232
+ constructor(options) {
2233
+ this.options = { ...DEFAULT_OPTIONS2, ...options };
2234
+ }
2235
+ async startServers(wtCtx, ports) {
2236
+ if (this.servers.has(wtCtx.issueIid)) {
2237
+ logger8.info("Servers already running for issue", { issueIid: wtCtx.issueIid });
2238
+ return;
2239
+ }
2240
+ logger8.info("Starting dev servers", { issueIid: wtCtx.issueIid, ...ports });
2241
+ const backendEnv = {
2242
+ ...process.env,
2243
+ PORT: String(ports.backendPort),
2244
+ E2E_PORT_OVERRIDE: "1",
2245
+ ENV_PATH: ".env.development.local"
2246
+ };
2247
+ const backend = spawn2("node", ["ace", "serve", "--watch"], {
2248
+ cwd: wtCtx.workDir,
2249
+ env: backendEnv,
2250
+ stdio: ["ignore", "pipe", "pipe"],
2251
+ detached: false
2252
+ });
2253
+ backend.stdout?.on("data", (data) => {
2254
+ logger8.debug(`[BE #${wtCtx.issueIid}] ${data.toString().trimEnd()}`);
2255
+ });
2256
+ backend.stderr?.on("data", (data) => {
2257
+ logger8.debug(`[BE #${wtCtx.issueIid} ERR] ${data.toString().trimEnd()}`);
2258
+ });
2259
+ backend.on("exit", (code) => {
2260
+ logger8.info("Backend process exited", { issueIid: wtCtx.issueIid, code });
2261
+ });
2262
+ const frontendDir = path8.join(wtCtx.workDir, "frontend");
2263
+ const frontendEnv = {
2264
+ ...process.env,
2265
+ BACKEND_PORT: String(ports.backendPort),
2266
+ FRONTEND_PORT: String(ports.frontendPort)
2267
+ };
2268
+ const frontend = spawn2("pnpm", ["dev", "--", "--port", String(ports.frontendPort)], {
2269
+ cwd: frontendDir,
2270
+ env: frontendEnv,
2271
+ stdio: ["ignore", "pipe", "pipe"],
2272
+ detached: false
2273
+ });
2274
+ frontend.stdout?.on("data", (data) => {
2275
+ logger8.debug(`[FE #${wtCtx.issueIid}] ${data.toString().trimEnd()}`);
2276
+ });
2277
+ frontend.stderr?.on("data", (data) => {
2278
+ logger8.debug(`[FE #${wtCtx.issueIid} ERR] ${data.toString().trimEnd()}`);
2279
+ });
2280
+ frontend.on("exit", (code) => {
2281
+ logger8.info("Frontend process exited", { issueIid: wtCtx.issueIid, code });
2282
+ });
2283
+ const serverSet = {
2284
+ backend,
2285
+ frontend,
2286
+ ports,
2287
+ workDir: wtCtx.workDir,
2288
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
2289
+ };
2290
+ this.servers.set(wtCtx.issueIid, serverSet);
2291
+ logger8.info("Waiting for servers to become healthy", { issueIid: wtCtx.issueIid });
2292
+ try {
2293
+ await Promise.all([
2294
+ waitForPort(
2295
+ ports.backendPort,
2296
+ false,
2297
+ this.options.healthCheckTimeoutMs,
2298
+ this.options.healthCheckIntervalMs
2299
+ ),
2300
+ waitForPort(
2301
+ ports.frontendPort,
2302
+ true,
2303
+ this.options.healthCheckTimeoutMs,
2304
+ this.options.healthCheckIntervalMs
2305
+ )
2306
+ ]);
2307
+ logger8.info("Dev servers healthy", { issueIid: wtCtx.issueIid, ...ports });
2308
+ } catch (err) {
2309
+ logger8.error("Dev servers failed health check, cleaning up", {
2310
+ issueIid: wtCtx.issueIid,
2311
+ error: err.message
2312
+ });
2313
+ this.stopServers(wtCtx.issueIid);
2314
+ throw err;
2315
+ }
2316
+ }
2317
+ stopServers(issueIid) {
2318
+ const set = this.servers.get(issueIid);
2319
+ if (!set) return;
2320
+ logger8.info("Stopping dev servers", { issueIid, ports: set.ports });
2321
+ killProcess(set.backend, `backend #${issueIid}`);
2322
+ killProcess(set.frontend, `frontend #${issueIid}`);
2323
+ this.servers.delete(issueIid);
2324
+ }
2325
+ stopAll() {
2326
+ for (const [iid] of this.servers) {
2327
+ this.stopServers(iid);
2328
+ }
2329
+ }
2330
+ getStatus(issueIid) {
2331
+ const set = this.servers.get(issueIid);
2332
+ if (!set) return { running: false };
2333
+ return {
2334
+ running: true,
2335
+ ports: set.ports,
2336
+ startedAt: set.startedAt
2337
+ };
2338
+ }
2339
+ getRunningIssues() {
2340
+ return [...this.servers.keys()];
2341
+ }
2342
+ };
2343
+ function killProcess(proc, label) {
2344
+ try {
2345
+ if (proc.killed || proc.exitCode !== null) return;
2346
+ proc.kill("SIGTERM");
2347
+ setTimeout(() => {
2348
+ if (!proc.killed && proc.exitCode === null) {
2349
+ logger8.warn(`Force killing ${label}`);
2350
+ proc.kill("SIGKILL");
2351
+ }
2352
+ }, 5e3);
2353
+ } catch (err) {
2354
+ logger8.warn(`Failed to kill ${label}`, { error: err.message });
2355
+ }
2356
+ }
2357
+
2358
+ // src/e2e/ScreenshotCollector.ts
2359
+ import fs7 from "fs";
2360
+ import path9 from "path";
2361
+ var logger9 = logger.child("ScreenshotCollector");
2362
+ var MAX_SCREENSHOTS = 20;
2363
+ function walkDir(dir, files = []) {
2364
+ for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
2365
+ const full = path9.join(dir, entry.name);
2366
+ if (entry.isDirectory()) {
2367
+ walkDir(full, files);
2368
+ } else if (entry.isFile() && entry.name.endsWith(".png")) {
2369
+ files.push(full);
2370
+ }
2371
+ }
2372
+ return files;
2373
+ }
2374
+ function collectScreenshots(workDir) {
2375
+ const testResultsDir = path9.join(workDir, "frontend", "test-results");
2376
+ if (!fs7.existsSync(testResultsDir)) {
2377
+ logger9.debug("test-results directory not found", { dir: testResultsDir });
2378
+ return [];
2379
+ }
2380
+ const pngFiles = walkDir(testResultsDir);
2381
+ if (pngFiles.length === 0) {
2382
+ logger9.debug("No screenshots found");
2383
+ return [];
2384
+ }
2385
+ const screenshots = pngFiles.map((filePath) => {
2386
+ const relative = path9.relative(testResultsDir, filePath);
2387
+ const testName = relative.split(path9.sep)[0] || path9.basename(filePath, ".png");
2388
+ return { filePath, testName };
2389
+ });
2390
+ if (screenshots.length > MAX_SCREENSHOTS) {
2391
+ logger9.warn("Too many screenshots, truncating", {
2392
+ total: screenshots.length,
2393
+ max: MAX_SCREENSHOTS
2394
+ });
2395
+ return screenshots.slice(0, MAX_SCREENSHOTS);
2396
+ }
2397
+ logger9.info("Screenshots collected", { count: screenshots.length });
2398
+ return screenshots;
2399
+ }
2400
+
2401
+ // src/e2e/ScreenshotPublisher.ts
2402
+ var logger10 = logger.child("ScreenshotPublisher");
2403
+ function buildComment(uploaded, truncated) {
2404
+ const lines = [t("screenshot.title"), ""];
2405
+ for (const item of uploaded) {
2406
+ lines.push(`### ${item.testName}`, "", item.markdown, "");
2407
+ }
2408
+ if (truncated) {
2409
+ lines.push(t("screenshot.truncated"), "");
2410
+ }
2411
+ return lines.join("\n");
2412
+ }
2413
+ var ScreenshotPublisher = class {
2414
+ constructor(gongfeng) {
2415
+ this.gongfeng = gongfeng;
2416
+ }
2417
+ async publish(options) {
2418
+ const { workDir, issueIid, issueId, mrIid } = options;
2419
+ const screenshots = collectScreenshots(workDir);
2420
+ if (screenshots.length === 0) {
2421
+ logger10.info("No E2E screenshots to publish", { issueIid });
2422
+ return;
2423
+ }
2424
+ const uploaded = await this.uploadAll(screenshots);
2425
+ if (uploaded.length === 0) {
2426
+ logger10.warn("All screenshot uploads failed", { issueIid });
2427
+ return;
2428
+ }
2429
+ const truncated = screenshots.length >= 20;
2430
+ const comment = buildComment(uploaded, truncated);
2431
+ await this.postToIssue(issueId, comment);
2432
+ if (mrIid) {
2433
+ await this.postToMergeRequest(mrIid, comment);
2434
+ }
2435
+ logger10.info("E2E screenshots published", {
2436
+ issueIid,
2437
+ mrIid,
2438
+ count: uploaded.length
2439
+ });
2440
+ }
2441
+ async uploadAll(screenshots) {
2442
+ const results = [];
2443
+ for (const screenshot of screenshots) {
2444
+ try {
2445
+ const result = await this.gongfeng.uploadFile(screenshot.filePath);
2446
+ results.push({
2447
+ testName: screenshot.testName,
2448
+ markdown: result.markdown
2449
+ });
2450
+ } catch (err) {
2451
+ logger10.warn("Failed to upload screenshot", {
2452
+ filePath: screenshot.filePath,
2453
+ error: err.message
2454
+ });
2455
+ }
2456
+ }
2457
+ return results;
2458
+ }
2459
+ async postToIssue(issueId, comment) {
2460
+ try {
2461
+ await this.gongfeng.createIssueNote(issueId, comment);
2462
+ } catch (err) {
2463
+ logger10.warn("Failed to post screenshots to issue", {
2464
+ issueId,
2465
+ error: err.message
2466
+ });
2467
+ }
2468
+ }
2469
+ async postToMergeRequest(mrIid, comment) {
2470
+ try {
2471
+ await this.gongfeng.createMergeRequestNote(mrIid, comment);
2472
+ } catch (err) {
2473
+ logger10.warn("Failed to post screenshots to merge request", {
2474
+ mrIid,
2475
+ error: err.message
2476
+ });
2477
+ }
2478
+ }
2479
+ };
2480
+
2481
+ // src/orchestrator/PipelineOrchestrator.ts
2482
+ var execFileAsync2 = promisify2(execFile2);
2483
+ var logger11 = logger.child("PipelineOrchestrator");
2484
+ var PipelineOrchestrator = class {
2485
+ config;
2486
+ gongfeng;
2487
+ mainGit;
2488
+ aiRunner;
2489
+ tracker;
2490
+ supplementStore;
2491
+ mainGitMutex = new AsyncMutex();
2492
+ pipelineDef;
2493
+ portAllocator;
2494
+ devServerManager;
2495
+ screenshotPublisher;
2496
+ constructor(config, gongfeng, git, aiRunner, tracker, supplementStore) {
2497
+ this.config = config;
2498
+ this.gongfeng = gongfeng;
2499
+ this.mainGit = git;
2500
+ this.aiRunner = aiRunner;
2501
+ this.tracker = tracker;
2502
+ this.supplementStore = supplementStore;
2503
+ const mode = resolvePipelineMode(config.ai.mode, config.pipeline?.mode === "auto" ? void 0 : config.pipeline?.mode);
2504
+ this.pipelineDef = getPipelineDef(mode);
2505
+ logger11.info("Pipeline mode resolved", { mode: this.pipelineDef.mode, aiMode: config.ai.mode });
2506
+ this.portAllocator = new PortAllocator({
2507
+ backendPortBase: config.e2e.backendPortBase,
2508
+ frontendPortBase: config.e2e.frontendPortBase
2509
+ });
2510
+ this.devServerManager = new DevServerManager();
2511
+ this.screenshotPublisher = new ScreenshotPublisher(gongfeng);
2512
+ this.restorePortAllocations();
2513
+ }
2514
+ getPortAllocator() {
2515
+ return this.portAllocator;
2516
+ }
2517
+ getDevServerManager() {
2518
+ return this.devServerManager;
2519
+ }
2520
+ restorePortAllocations() {
2521
+ for (const record of this.tracker.getAll()) {
2522
+ if (record.ports) {
2523
+ this.portAllocator.restore(record.issueIid, record.ports);
2524
+ }
2525
+ }
2526
+ }
2527
+ getPipelineDef() {
2528
+ return this.pipelineDef;
2529
+ }
2530
+ emitProgress(issueIid, step, message) {
2531
+ eventBus.emitTyped("pipeline:progress", { issueIid, step, message });
2532
+ }
2533
+ computeWorktreeContext(issueIid, branchName) {
2534
+ const gitRootDir = path10.join(this.config.project.worktreeBaseDir, `issue-${issueIid}`);
2535
+ const workDir = path10.join(gitRootDir, this.config.project.projectSubDir);
2536
+ return { gitRootDir, workDir, branchName, issueIid };
2537
+ }
2538
+ async ensureWorktree(wtCtx) {
2539
+ const worktrees = await this.mainGit.worktreeList();
2540
+ if (worktrees.includes(wtCtx.gitRootDir)) {
2541
+ logger11.info("Reusing existing worktree", { dir: wtCtx.gitRootDir });
2542
+ return;
2543
+ }
2544
+ const localExists = await this.mainGit.branchExists(wtCtx.branchName);
2545
+ if (localExists) {
2546
+ await this.mainGit.worktreeAddExisting(wtCtx.gitRootDir, wtCtx.branchName);
2547
+ return;
2548
+ }
2549
+ const remoteExists = await this.mainGit.remoteBranchExists(wtCtx.branchName);
2550
+ if (remoteExists) {
2551
+ await this.mainGit.worktreeAddTracking(wtCtx.gitRootDir, wtCtx.branchName);
2552
+ return;
2553
+ }
2554
+ await this.mainGit.worktreeAdd(
2555
+ wtCtx.gitRootDir,
2556
+ wtCtx.branchName,
2557
+ `origin/${this.config.project.baseBranch}`
2558
+ );
2559
+ }
2560
+ async cleanupWorktree(wtCtx) {
2561
+ try {
2562
+ await this.mainGit.worktreeRemove(wtCtx.gitRootDir, true);
2563
+ logger11.info("Worktree cleaned up", { dir: wtCtx.gitRootDir });
2564
+ } catch (err) {
2565
+ logger11.warn("Failed to cleanup worktree", { dir: wtCtx.gitRootDir, error: err.message });
2566
+ }
2567
+ }
2568
+ async installDependencies(workDir) {
2569
+ logger11.info("Installing dependencies in worktree", { workDir });
2570
+ const seeded = await this.seedNodeModulesFromMain(workDir);
2571
+ if (seeded) {
2572
+ logger11.info("node_modules seeded from main repo \u2014 skipping pnpm install");
2573
+ return;
2574
+ }
2575
+ try {
2576
+ await execFileAsync2("pnpm", ["install", "--frozen-lockfile"], {
2577
+ cwd: workDir,
2578
+ maxBuffer: 10 * 1024 * 1024,
2579
+ timeout: 3e5
2580
+ });
2581
+ logger11.info("Dependencies installed");
2582
+ } catch (err) {
2583
+ logger11.warn("pnpm install --frozen-lockfile failed, retrying with --ignore-scripts", {
2584
+ error: err.message
2585
+ });
2586
+ try {
2587
+ await execFileAsync2("pnpm", ["install", "--frozen-lockfile", "--ignore-scripts"], {
2588
+ cwd: workDir,
2589
+ maxBuffer: 10 * 1024 * 1024,
2590
+ timeout: 3e5
2591
+ });
2592
+ logger11.info("Dependencies installed (scripts ignored)");
2593
+ } catch (retryErr) {
2594
+ logger11.warn("pnpm install also failed with --ignore-scripts", {
2595
+ error: retryErr.message
2596
+ });
2597
+ }
2598
+ }
2599
+ }
2600
+ async seedNodeModulesFromMain(workDir) {
2601
+ const targetBin = path10.join(workDir, "node_modules", ".bin");
2602
+ try {
2603
+ await fs8.access(targetBin);
2604
+ logger11.info("Worktree node_modules already complete (has .bin/), skipping seed");
2605
+ return false;
2606
+ } catch {
2607
+ }
2608
+ const sourceNM = path10.join(this.config.project.workDir, "node_modules");
2609
+ const targetNM = path10.join(workDir, "node_modules");
2610
+ try {
2611
+ await fs8.access(sourceNM);
2612
+ } catch {
2613
+ logger11.warn("Main repo node_modules not found, skipping seed", { sourceNM });
2614
+ return false;
2615
+ }
2616
+ logger11.info("Seeding node_modules from main repo via reflink copy", { sourceNM, targetNM });
2617
+ try {
2618
+ await execFileAsync2("rm", ["-rf", targetNM], { timeout: 6e4 });
2619
+ await execFileAsync2("cp", ["-a", "--reflink=auto", sourceNM, targetNM], {
2620
+ timeout: 12e4
2621
+ });
2622
+ logger11.info("node_modules seeded from main repo");
2623
+ return true;
2624
+ } catch (err) {
2625
+ logger11.warn("Failed to seed node_modules from main repo", {
2626
+ error: err.message
2627
+ });
2628
+ return false;
2629
+ }
2630
+ }
2631
+ async restartIssue(issueIid) {
2632
+ const record = this.tracker.get(issueIid);
2633
+ if (!record) throw new Error(`Issue ${issueIid} not found in tracker`);
2634
+ const wtCtx = this.computeWorktreeContext(issueIid, record.branchName);
2635
+ logger11.info("Restarting issue \u2014 cleaning context", { issueIid, branchName: record.branchName });
2636
+ this.stopPreviewServers(issueIid);
2637
+ try {
2638
+ const deleted = await this.gongfeng.cleanupAgentNotes(record.issueId);
2639
+ logger11.info("Agent notes cleaned up", { issueIid, deleted });
2640
+ } catch (err) {
2641
+ logger11.warn("Failed to cleanup agent notes", { issueIid, error: err.message });
2642
+ }
2643
+ await this.mainGitMutex.runExclusive(async () => {
2644
+ await this.cleanupWorktree(wtCtx);
2645
+ try {
2646
+ await this.mainGit.deleteBranch(record.branchName);
2647
+ } catch {
2648
+ }
2649
+ try {
2650
+ await this.mainGit.deleteRemoteBranch(record.branchName);
2651
+ } catch {
2652
+ }
2653
+ });
2654
+ this.tracker.resetFull(issueIid);
2655
+ logger11.info("Issue restarted", { issueIid });
2656
+ }
2657
+ retryFromPhase(issueIid, phase) {
2658
+ const record = this.tracker.get(issueIid);
2659
+ if (!record) throw new Error(`Issue ${issueIid} not found in tracker`);
2660
+ const issueDef = this.getIssueSpecificPipelineDef(record);
2661
+ const spec = issueDef.phases.find((p) => p.name === phase);
2662
+ if (!spec || spec.kind !== "ai") {
2663
+ throw new Error(`Invalid phase for retry: ${phase}`);
2664
+ }
2665
+ logger11.info("Retrying issue from phase", { issueIid, phase });
2666
+ this.tracker.resetToPhase(issueIid, phase, issueDef);
2667
+ }
2668
+ getIssueSpecificPipelineDef(record) {
2669
+ if (record.pipelineMode === "plan-mode" || record.pipelineMode === "classic") {
2670
+ return getPipelineDef(record.pipelineMode);
2671
+ }
2672
+ return this.pipelineDef;
2673
+ }
2674
+ async processIssue(issue) {
2675
+ const branchName = `${this.config.project.branchPrefix}-${issue.iid}`;
2676
+ const wtCtx = this.computeWorktreeContext(issue.iid, branchName);
2677
+ logger11.info("Processing issue", { iid: issue.iid, title: issue.title, branchName, worktree: wtCtx.gitRootDir });
2678
+ let record = this.tracker.get(issue.iid);
2679
+ const isRetry = record?.state === "failed" /* Failed */;
2680
+ if (!record) {
2681
+ record = this.tracker.create({
2682
+ issueId: issue.id,
2683
+ issueIid: issue.iid,
2684
+ issueTitle: issue.title,
2685
+ state: "pending" /* Pending */,
2686
+ branchName,
2687
+ pipelineMode: this.pipelineDef.mode
2688
+ });
2689
+ }
2690
+ if (!record.pipelineMode) {
2691
+ this.tracker.updateState(issue.iid, record.state, { pipelineMode: this.pipelineDef.mode });
2692
+ record.pipelineMode = this.pipelineDef.mode;
2693
+ }
2694
+ const issuePipelineDef = this.getIssueSpecificPipelineDef(record);
2695
+ try {
2696
+ await this.gongfeng.updateIssueLabels(issue.id, [
2697
+ ...issue.labels.filter((l) => !l.startsWith("auto-finish:")),
2698
+ "auto-finish:processing"
2699
+ ]);
2700
+ } catch (err) {
2701
+ logger11.warn("Failed to update issue labels", { error: err.message });
2702
+ }
2703
+ try {
2704
+ await this.gongfeng.createIssueNote(
2705
+ issue.id,
2706
+ isRetry ? t("orchestrator.retryComment") : t("orchestrator.startComment")
2707
+ );
2708
+ } catch {
2709
+ }
2710
+ const supplementText = this.supplementStore?.toPromptText(issue.iid) ?? "";
2711
+ const ctx = {
2712
+ issueIid: issue.iid,
2713
+ issueId: issue.id,
2714
+ issueTitle: issue.title,
2715
+ issueDescription: issue.description || "",
2716
+ branchName,
2717
+ supplementText: supplementText || void 0,
2718
+ pipelineMode: issuePipelineDef.mode
2719
+ };
2720
+ try {
2721
+ await this.mainGitMutex.runExclusive(async () => {
2722
+ this.emitProgress(issue.iid, "fetch", t("orchestrator.fetchProgress"));
2723
+ await this.mainGit.fetch();
2724
+ this.emitProgress(issue.iid, "worktree", t("orchestrator.worktreeProgress"));
2725
+ await this.ensureWorktree(wtCtx);
2726
+ });
2727
+ if (record.state === "pending" /* Pending */) {
2728
+ this.tracker.updateState(issue.iid, "branch_created" /* BranchCreated */);
2729
+ }
2730
+ this.emitProgress(issue.iid, "install", t("orchestrator.installProgress"));
2731
+ await this.installDependencies(wtCtx.workDir);
2732
+ this.emitProgress(issue.iid, "init_plan", t("orchestrator.initPlanProgress"));
2733
+ const wtGit = new GitOperations(wtCtx.gitRootDir);
2734
+ const wtPlan = new PlanPersistence(wtCtx.workDir, issue.iid);
2735
+ wtPlan.ensureDir();
2736
+ wtPlan.writeIssueMeta({
2737
+ id: issue.id,
2738
+ iid: issue.iid,
2739
+ title: issue.title,
2740
+ labels: issue.labels,
2741
+ state: issue.state
2742
+ });
2743
+ const existingProgress = wtPlan.readProgress();
2744
+ if (!existingProgress) {
2745
+ wtPlan.writeProgress(
2746
+ wtPlan.createInitialProgress(issue.id, issue.title, branchName, issuePipelineDef)
2747
+ );
2748
+ }
2749
+ const startIdx = this.determineStartIndex(record.state, isRetry ? record.failedAtState : void 0, issuePipelineDef);
2750
+ this.emitProgress(issue.iid, "phase_start", t("orchestrator.phaseStartProgress", { phase: issuePipelineDef.phases[startIdx]?.name ?? "start" }));
2751
+ const needsDeployment = this.shouldDeployServers(issue.iid);
2752
+ let serversStarted = false;
2753
+ for (let i = startIdx; i < issuePipelineDef.phases.length; i++) {
2754
+ const spec = issuePipelineDef.phases[i];
2755
+ if (spec.kind === "gate") {
2756
+ if (this.shouldAutoApprove(issue.labels)) {
2757
+ logger11.info("Auto-approving review gate (matched autoApproveLabels)", {
2758
+ iid: issue.iid,
2759
+ labels: issue.labels,
2760
+ autoApproveLabels: this.config.review.autoApproveLabels
2761
+ });
2762
+ if (spec.approvedState) {
2763
+ this.tracker.updateState(issue.iid, spec.approvedState);
2764
+ }
2765
+ wtPlan.updatePhaseProgress(spec.name, "completed");
2766
+ try {
2767
+ await this.gongfeng.createIssueNote(
2768
+ issue.id,
2769
+ t("orchestrator.autoApproveComment")
2770
+ );
2771
+ } catch {
2772
+ }
2773
+ continue;
2774
+ }
2775
+ this.tracker.updateState(issue.iid, spec.startState);
2776
+ wtPlan.updatePhaseProgress(spec.name, "in_progress");
2777
+ eventBus.emitTyped("review:requested", { issueIid: issue.iid });
2778
+ logger11.info("Review gate reached, pausing", { iid: issue.iid });
2779
+ return;
2780
+ }
2781
+ const phase = createPhase(spec.name, this.aiRunner, wtGit, wtPlan, this.gongfeng, this.tracker, this.config);
2782
+ await phase.execute(ctx);
2783
+ if (needsDeployment && !serversStarted && this.isImplementDonePhase(spec.name, spec.doneState)) {
2784
+ const ports = await this.startPreviewServers(wtCtx, issue);
2785
+ if (ports) {
2786
+ ctx.ports = ports;
2787
+ wtCtx.ports = ports;
2788
+ serversStarted = true;
2789
+ }
2790
+ }
2791
+ }
2792
+ this.emitProgress(issue.iid, "create_mr", t("orchestrator.createMrProgress"));
2793
+ const previewUrl = this.buildPreviewUrl(issue.iid);
2794
+ const mrResult = await this.tryCreateMergeRequest(issue, branchName, wtCtx.workDir, previewUrl);
2795
+ const mrUrl = mrResult?.url ?? null;
2796
+ this.tracker.updateState(issue.iid, "completed" /* Completed */, mrUrl ? { mrUrl } : void 0);
2797
+ try {
2798
+ await this.gongfeng.updateIssueLabels(issue.id, [
2799
+ ...issue.labels.filter((l) => !l.startsWith("auto-finish:") && l !== "auto-finish"),
2800
+ "auto-finish:done"
2801
+ ]);
2802
+ } catch {
2803
+ }
2804
+ if (isE2eEnabledForIssue(issue.iid, this.tracker, this.config)) {
2805
+ this.emitProgress(issue.iid, "screenshots", t("orchestrator.uploadScreenshotsProgress"));
2806
+ try {
2807
+ await this.screenshotPublisher.publish({
2808
+ workDir: wtCtx.workDir,
2809
+ issueIid: issue.iid,
2810
+ issueId: issue.id,
2811
+ mrIid: mrResult?.iid
2812
+ });
2813
+ } catch (err) {
2814
+ logger11.warn("Failed to publish E2E screenshots", {
2815
+ iid: issue.iid,
2816
+ error: err.message
2817
+ });
2818
+ }
2819
+ }
2820
+ const mrSection = mrUrl ? t("orchestrator.mrSection", { mrUrl }) : t("orchestrator.mrFailSection");
2821
+ const previewSection = previewUrl ? `
2822
+ \u{1F310} Preview: ${previewUrl}` : "";
2823
+ try {
2824
+ await this.gongfeng.createIssueNote(
2825
+ issue.id,
2826
+ t("orchestrator.completedComment", { branch: branchName, mrSection, previewSection })
2827
+ );
2828
+ } catch {
2829
+ }
2830
+ if (serversStarted && this.config.preview.keepAfterComplete) {
2831
+ logger11.info("Preview servers kept running after completion", { iid: issue.iid });
2832
+ } else {
2833
+ this.stopPreviewServers(issue.iid);
2834
+ await this.mainGitMutex.runExclusive(() => this.cleanupWorktree(wtCtx));
2835
+ }
2836
+ logger11.info("Issue processing completed", { iid: issue.iid });
2837
+ } catch (err) {
2838
+ const errorMsg = err.message;
2839
+ logger11.error("Issue processing failed", { iid: issue.iid, error: errorMsg });
2840
+ const currentRecord = this.tracker.get(issue.iid);
2841
+ const failedAtState = currentRecord?.state || "pending" /* Pending */;
2842
+ if (failedAtState !== "failed" /* Failed */) {
2843
+ this.tracker.markFailed(issue.iid, errorMsg.slice(0, 500), failedAtState);
2844
+ }
2845
+ try {
2846
+ await this.gongfeng.updateIssueLabels(issue.id, [
2847
+ ...issue.labels.filter((l) => !l.startsWith("auto-finish:") && l !== "auto-finish"),
2848
+ "auto-finish",
2849
+ "auto-finish:failed"
2850
+ ]);
2851
+ } catch {
2852
+ }
2853
+ try {
2854
+ await this.gongfeng.createIssueNote(
2855
+ issue.id,
2856
+ t("orchestrator.failedComment", { error: errorMsg })
2857
+ );
2858
+ } catch {
2859
+ }
2860
+ logger11.info("Worktree preserved for debugging", { dir: wtCtx.gitRootDir });
2861
+ throw err;
2862
+ }
2863
+ }
2864
+ async tryCreateMergeRequest(issue, branchName, workDir, previewUrl) {
2865
+ try {
2866
+ const supplement = this.supplementStore?.get(issue.iid);
2867
+ const tapdId = supplement?.tapdId?.trim() || extractTapdId(issue.description || "");
2868
+ const title = generateMRTitle(issue.iid, issue.title, tapdId);
2869
+ let description = generateMRDescription({
2870
+ issueIid: issue.iid,
2871
+ issueTitle: issue.title,
2872
+ issueDescription: issue.description || "",
2873
+ branchName,
2874
+ planDir: workDir
2875
+ });
2876
+ if (previewUrl) {
2877
+ description += `
2878
+
2879
+ ## Preview Environment
2880
+
2881
+ \u{1F310} ${previewUrl}`;
2882
+ }
2883
+ const mr = await this.gongfeng.createMergeRequest({
2884
+ sourceBranch: branchName,
2885
+ targetBranch: this.config.project.baseBranch,
2886
+ title,
2887
+ description
2888
+ });
2889
+ logger11.info("Merge request created successfully", {
2890
+ iid: issue.iid,
2891
+ mrIid: mr.iid,
2892
+ mrUrl: mr.web_url
2893
+ });
2894
+ return { url: mr.web_url, iid: mr.iid };
2895
+ } catch (err) {
2896
+ const errorMsg = err.message;
2897
+ logger11.warn("Failed to create merge request, trying to find existing one", {
2898
+ iid: issue.iid,
2899
+ error: errorMsg
2900
+ });
2901
+ if (errorMsg.includes("already exists")) {
2902
+ return this.tryFindExistingMergeRequest(issue.iid, branchName);
2903
+ }
2904
+ return null;
2905
+ }
2906
+ }
2907
+ async tryFindExistingMergeRequest(issueIid, branchName) {
2908
+ try {
2909
+ const existing = await this.gongfeng.findMergeRequestByBranch(
2910
+ branchName,
2911
+ this.config.project.baseBranch
2912
+ );
2913
+ if (existing) {
2914
+ logger11.info("Found existing merge request", {
2915
+ iid: issueIid,
2916
+ mrIid: existing.iid,
2917
+ mrUrl: existing.web_url
2918
+ });
2919
+ return { url: existing.web_url, iid: existing.iid };
2920
+ }
2921
+ } catch (findErr) {
2922
+ logger11.warn("Failed to find existing merge request", {
2923
+ iid: issueIid,
2924
+ error: findErr.message
2925
+ });
2926
+ }
2927
+ return null;
2928
+ }
2929
+ determineStartIndex(state, failedAtState, def) {
2930
+ const target = failedAtState || state;
2931
+ for (let i = def.phases.length - 1; i >= 0; i--) {
2932
+ const spec = def.phases[i];
2933
+ if (spec.kind === "gate" && spec.approvedState === target) {
2934
+ return i + 1;
2935
+ }
2936
+ if (spec.startState === target || spec.doneState === target) {
2937
+ return spec.doneState === target ? i + 1 : i;
2938
+ }
2939
+ }
2940
+ return 0;
2941
+ }
2942
+ shouldDeployServers(issueIid) {
2943
+ return isE2eEnabledForIssue(issueIid, this.tracker, this.config) || this.config.preview.enabled;
2944
+ }
2945
+ shouldAutoApprove(issueLabels) {
2946
+ const autoLabels = this.config.review.autoApproveLabels;
2947
+ if (!autoLabels.length) return false;
2948
+ return issueLabels.some((l) => autoLabels.includes(l));
2949
+ }
2950
+ isImplementDonePhase(phaseName, doneState) {
2951
+ return doneState === "implement_done" /* ImplementDone */ || doneState === "build_done" /* BuildDone */;
2952
+ }
2953
+ async startPreviewServers(wtCtx, issue) {
2954
+ try {
2955
+ this.emitProgress(issue.iid, "deploy", t("orchestrator.deployProgress"));
2956
+ const ports = await this.portAllocator.allocate(issue.iid);
2957
+ wtCtx.ports = ports;
2958
+ this.tracker.updateState(issue.iid, this.tracker.get(issue.iid).state, {
2959
+ ports,
2960
+ previewStartedAt: (/* @__PURE__ */ new Date()).toISOString()
2961
+ });
2962
+ await this.devServerManager.startServers(wtCtx, ports);
2963
+ const previewUrl = this.buildPreviewUrl(issue.iid);
2964
+ if (previewUrl) {
2965
+ try {
2966
+ await this.gongfeng.createIssueNote(
2967
+ issue.id,
2968
+ this.buildPreviewComment(ports, previewUrl)
2969
+ );
2970
+ } catch {
2971
+ }
2972
+ }
2973
+ this.emitProgress(issue.iid, "deploy_done", t("orchestrator.deployDoneProgress", { url: previewUrl ?? "N/A" }));
2974
+ eventBus.emitTyped("pipeline:progress", {
2975
+ issueIid: issue.iid,
2976
+ step: "preview_ready",
2977
+ message: previewUrl ?? ""
2978
+ });
2979
+ return ports;
2980
+ } catch (err) {
2981
+ logger11.error("Failed to start preview servers", {
2982
+ iid: issue.iid,
2983
+ error: err.message
2984
+ });
2985
+ return null;
2986
+ }
2987
+ }
2988
+ stopPreviewServers(issueIid) {
2989
+ this.devServerManager.stopServers(issueIid);
2990
+ this.portAllocator.release(issueIid);
2991
+ const record = this.tracker.get(issueIid);
2992
+ if (record?.ports) {
2993
+ this.tracker.updateState(issueIid, record.state, {
2994
+ ports: void 0,
2995
+ previewStartedAt: void 0
2996
+ });
2997
+ }
2998
+ }
2999
+ getPreviewHost() {
3000
+ if (this.config.preview.host) return this.config.preview.host;
3001
+ const interfaces = os3.networkInterfaces();
3002
+ for (const addrs of Object.values(interfaces)) {
3003
+ for (const addr of addrs ?? []) {
3004
+ if (addr.family === "IPv4" && !addr.internal) {
3005
+ return addr.address;
3006
+ }
3007
+ }
3008
+ }
3009
+ return "localhost";
3010
+ }
3011
+ buildPreviewUrl(issueIid) {
3012
+ const ports = this.portAllocator.getPortsForIssue(issueIid);
3013
+ if (!ports) return null;
3014
+ const host = this.getPreviewHost();
3015
+ return `https://${host}:${ports.frontendPort}`;
3016
+ }
3017
+ buildPreviewComment(ports, previewUrl) {
3018
+ const host = this.getPreviewHost();
3019
+ const ttlHours = Math.round(this.config.preview.ttlMs / (60 * 60 * 1e3));
3020
+ return [
3021
+ t("orchestrator.previewComment.title"),
3022
+ "",
3023
+ t("orchestrator.previewComment.tableHeader"),
3024
+ t("orchestrator.previewComment.tableSep"),
3025
+ `| ${t("orchestrator.previewComment.frontend")} | ${previewUrl} |`,
3026
+ `| ${t("orchestrator.previewComment.backendApi")} | http://${host}:${ports.backendPort}/api |`,
3027
+ "",
3028
+ t("orchestrator.previewComment.hint"),
3029
+ t("orchestrator.previewComment.expiry", { hours: ttlHours })
3030
+ ].join("\n");
3031
+ }
3032
+ };
3033
+
3034
+ // src/services/BrainstormService.ts
3035
+ import { randomUUID } from "crypto";
3036
+
3037
+ // src/prompts/brainstorm-templates.ts
3038
+ function buildGeneratePrompt(transcript) {
3039
+ return `\u4F60\u662F\u4E00\u4E2A\u8D44\u6DF1\u8F6F\u4EF6\u67B6\u6784\u5E08\u3002\u4E0B\u9762\u662F\u7528\u6237\u901A\u8FC7\u8BED\u97F3\u503E\u5012\u7684\u4E00\u6BB5\u5173\u4E8E\u8F6F\u4EF6\u9700\u6C42\u7684\u60F3\u6CD5\uFF0C\u5185\u5BB9\u53EF\u80FD\u4E0D\u8FDE\u8D2F\u3001\u6709\u91CD\u590D\u3001\u5939\u6742\u8BED\u6C14\u8BCD\u2014\u2014\u8FD9\u662F\u6B63\u5E38\u7684\u3002
3040
+
3041
+ **\u6838\u5FC3\u539F\u5219\u2014\u2014\u7B2C\u4E00\u6027\u539F\u7406\u601D\u8003\uFF1A**
3042
+ - \u4E0D\u8981\u5047\u8BBE\u7528\u6237\u975E\u5E38\u6E05\u695A\u81EA\u5DF1\u60F3\u8981\u4EC0\u4E48\u4EE5\u53CA\u8BE5\u600E\u4E48\u5F97\u5230\u3002\u7528\u6237\u5F80\u5F80\u53EA\u6709\u6A21\u7CCA\u7684\u610F\u56FE\uFF0C\u4F60\u7684\u5DE5\u4F5C\u662F\u4ECE\u539F\u59CB\u9700\u6C42\u548C\u5E95\u5C42\u95EE\u9898\u51FA\u53D1\uFF0C\u5E2E\u4ED6\u5398\u6E05\u771F\u6B63\u7684\u76EE\u6807\u3002
3043
+ - \u5982\u679C\u7528\u6237\u7684\u52A8\u673A\u548C\u76EE\u6807\u4E0D\u6E05\u6670\uFF0C\u5728 SDD \u4E2D\u660E\u786E\u6807\u6CE8 [\u5F85\u6F84\u6E05]\uFF0C\u8BF4\u660E\u4E3A\u4EC0\u4E48\u8FD9\u91CC\u9700\u8981\u8FDB\u4E00\u6B65\u8BA8\u8BBA\uFF0C\u5E76\u7ED9\u51FA\u4F60\u7684\u7406\u89E3\u548C\u5EFA\u8BAE\u3002
3044
+ - \u5982\u679C\u76EE\u6807\u6E05\u6670\u4F46\u7528\u6237\u63CF\u8FF0\u7684\u8DEF\u5F84\u4E0D\u662F\u6700\u4F18\u89E3\uFF0C\u6307\u51FA\u6765\uFF0C\u5E76\u5EFA\u8BAE\u66F4\u597D\u7684\u65B9\u6848\u3002\u7528 [\u66F4\u4F18\u8DEF\u5F84] \u6807\u6CE8\u8FD9\u7C7B\u5EFA\u8BAE\u3002
3045
+ - \u56DE\u5230\u95EE\u9898\u7684\u672C\u8D28\uFF1A\u5148\u95EE"\u7528\u6237\u771F\u6B63\u8981\u89E3\u51B3\u7684\u95EE\u9898\u662F\u4EC0\u4E48"\uFF0C\u518D\u60F3"\u6700\u7B80\u5355\u6709\u6548\u7684\u65B9\u6848\u662F\u4EC0\u4E48"\uFF0C\u907F\u514D\u8FC7\u5EA6\u8BBE\u8BA1\u3002
3046
+
3047
+ \u8BF7\u4F60\uFF1A
3048
+ 1. \u9996\u5148\u4F7F\u7528 Read\u3001Grep\u3001Glob \u7B49\u5DE5\u5177\u6D4F\u89C8\u9879\u76EE\u4EE3\u7801\u5E93\uFF0C\u4E86\u89E3\u73B0\u6709\u67B6\u6784\u3001\u6280\u672F\u6808\u548C\u4EE3\u7801\u98CE\u683C
3049
+ 2. \u5206\u6790\u7528\u6237\u60F3\u6CD5\u7684\u5E95\u5C42\u52A8\u673A\u2014\u2014\u4ED6\u8981\u89E3\u51B3\u4EC0\u4E48\u95EE\u9898\uFF1F\u4E3A\u4EC0\u4E48\u8981\u89E3\u51B3\uFF1F\u73B0\u6709\u65B9\u6848\u4E3A\u4EC0\u4E48\u4E0D\u591F\uFF1F
3050
+ 3. \u57FA\u4E8E\u5BF9\u4EE3\u7801\u5E93\u7684\u7406\u89E3\u548C\u7528\u6237\u7684\u771F\u5B9E\u9700\u6C42\uFF0C\u751F\u6210\u4E00\u4EFD\u7ED3\u6784\u5316\u7684\u8F6F\u4EF6\u8BBE\u8BA1\u6587\u6863 (SDD)
3051
+
3052
+ SDD \u8F93\u51FA\u8981\u6C42\uFF1A
3053
+ - \u4F7F\u7528 Markdown \u683C\u5F0F
3054
+ - \u5305\u542B\u4EE5\u4E0B\u7AE0\u8282\uFF1A\u6982\u8FF0\u3001\u76EE\u6807\u4E0E\u8303\u56F4\u3001\u6280\u672F\u65B9\u6848\uFF08\u542B\u67B6\u6784\u8BBE\u8BA1\u3001\u6570\u636E\u6A21\u578B\u3001\u63A5\u53E3\u8BBE\u8BA1\uFF09\u3001\u5B9E\u65BD\u8BA1\u5212\u3001\u98CE\u9669\u4E0E\u7EA6\u675F
3055
+ - \u5728"\u6982\u8FF0"\u7AE0\u8282\u4E2D\uFF0C\u5148\u7528 2-3 \u53E5\u8BDD\u9610\u660E\u4F60\u7406\u89E3\u7684\u7528\u6237\u6838\u5FC3\u8BC9\u6C42\u548C\u5E95\u5C42\u52A8\u673A\uFF0C\u518D\u5C55\u5F00\u65B9\u6848
3056
+ - \u7ED3\u5408\u9879\u76EE\u73B0\u6709\u4EE3\u7801\u7ED3\u6784\u7ED9\u51FA\u5177\u4F53\u7684\u6587\u4EF6\u8DEF\u5F84\u548C\u6A21\u5757\u5EFA\u8BAE
3057
+ - \u5982\u679C\u7528\u6237\u7684\u60F3\u6CD5\u4E2D\u6709\u6A21\u7CCA\u6216\u77DB\u76FE\u7684\u5730\u65B9\uFF0C\u5148\u505A\u5408\u7406\u63A8\u65AD\u5E76\u6807\u6CE8 [\u63A8\u65AD]
3058
+ - \u5982\u679C\u53D1\u73B0\u66F4\u77ED\u7684\u5B9E\u73B0\u8DEF\u5F84\u6216\u66F4\u7B80\u6D01\u7684\u65B9\u6848\uFF0C\u7528 [\u66F4\u4F18\u8DEF\u5F84] \u6807\u6CE8\u5E76\u8BF4\u660E\u7406\u7531
3059
+
3060
+ \u7528\u6237\u7684\u60F3\u6CD5\u539F\u6587\uFF1A
3061
+ ---
3062
+ ${transcript}
3063
+ ---
3064
+
3065
+ \u8BF7\u76F4\u63A5\u8F93\u51FA\u5B8C\u6574\u7684 SDD \u6587\u6863\u5185\u5BB9\uFF08\u7EAF Markdown \u6587\u672C\uFF09\uFF0C\u4E0D\u8981\u8F93\u51FA\u4EFB\u4F55\u591A\u4F59\u89E3\u91CA\u3002`;
3066
+ }
3067
+ function buildReviewPrompt(sdd, round) {
3068
+ return `\u4F60\u662F\u4E00\u4E2A\u4E25\u8C28\u7684\u6280\u672F\u8BC4\u5BA1\u4E13\u5BB6\u3002\u8BF7\u4F60\u4ED4\u7EC6\u5BA1\u67E5\u4EE5\u4E0B\u8F6F\u4EF6\u8BBE\u8BA1\u6587\u6863 (SDD)\u3002
3069
+
3070
+ \u8BF7\u4F60\uFF1A
3071
+ 1. \u4F7F\u7528 Read\u3001Grep\u3001Glob \u7B49\u5DE5\u5177\u6D4F\u89C8\u9879\u76EE\u4EE3\u7801\u5E93\uFF0C\u9A8C\u8BC1 SDD \u4E2D\u63D0\u5230\u7684\u6A21\u5757\u3001\u8DEF\u5F84\u3001\u63A5\u53E3\u662F\u5426\u4E0E\u73B0\u6709\u4EE3\u7801\u4E00\u81F4
3072
+ 2. \u7528\u7B2C\u4E00\u6027\u539F\u7406\u5BA1\u89C6\uFF1A\u9700\u6C42\u672C\u8EAB\u662F\u5426\u5408\u7406\uFF1F\u76EE\u6807\u662F\u5426\u6E05\u6670\uFF1F\u662F\u5426\u5B58\u5728\u66F4\u7B80\u5355\u76F4\u63A5\u7684\u5B9E\u73B0\u8DEF\u5F84\u88AB\u5FFD\u7565\u4E86\uFF1F
3073
+ 3. \u7AD9\u5728\u5B9E\u9645\u5B9E\u65BD\u8005\u7684\u89D2\u5EA6\uFF0C\u627E\u51FA\u6587\u6863\u4E2D**\u672A\u8BF4\u6E05\u695A\u3001\u542B\u7CCA\u4E0D\u6E05\u3001\u7F3A\u5931\u3001\u6216\u53EF\u80FD\u5BFC\u81F4\u5B9E\u65BD\u56F0\u96BE**\u7684\u5730\u65B9
3074
+ 4. \u5217\u51FA\u6070\u597D 20 \u4E2A\u95EE\u9898\uFF0C\u6309\u4E25\u91CD\u7A0B\u5EA6\u6392\u5E8F\uFF08\u6700\u91CD\u8981\u7684\u5728\u524D\uFF09
3075
+
3076
+ \u8FD9\u662F\u7B2C ${round} \u8F6E\u5BA1\u67E5${round > 1 ? "\uFF0C\u8BF7\u5173\u6CE8\u6BD4\u4E4B\u524D\u66F4\u7EC6\u7C92\u5EA6\u7684\u95EE\u9898\uFF0C\u7279\u522B\u662F\u8FB9\u754C\u6761\u4EF6\u3001\u9519\u8BEF\u5904\u7406\u3001\u6027\u80FD\u3001\u5B89\u5168\u7B49\u65B9\u9762" : ""}\u3002
3077
+
3078
+ SDD \u6587\u6863\uFF1A
3079
+ ---
3080
+ ${sdd}
3081
+ ---
3082
+
3083
+ \u8F93\u51FA\u683C\u5F0F\u8981\u6C42\uFF1A
3084
+ \u6BCF\u4E2A\u95EE\u9898\u7528\u7F16\u53F7\u5217\u51FA\uFF0C\u683C\u5F0F\u4E3A\uFF1A
3085
+ 1. [\u7C7B\u522B] \u95EE\u9898\u63CF\u8FF0
3086
+
3087
+ \u7C7B\u522B\u5305\u62EC\uFF1A[\u7F3A\u5931]\u3001[\u6A21\u7CCA]\u3001[\u77DB\u76FE]\u3001[\u98CE\u9669]\u3001[\u5EFA\u8BAE]\u3001[\u52A8\u673A]\uFF08\u9700\u6C42\u52A8\u673A\u6216\u76EE\u6807\u672C\u8EAB\u9700\u8981\u8D28\u7591\u65F6\uFF09\u3001[\u8FC7\u5EA6\u8BBE\u8BA1]\uFF08\u5B58\u5728\u66F4\u7B80\u5355\u8DEF\u5F84\u65F6\uFF09
3088
+
3089
+ \u8BF7\u76F4\u63A5\u8F93\u51FA 20 \u4E2A\u95EE\u9898\uFF0C\u4E0D\u8981\u8F93\u51FA\u591A\u4F59\u89E3\u91CA\u3002`;
3090
+ }
3091
+ function buildRefinePrompt(questions) {
3092
+ return `\u4E00\u4F4D\u6280\u672F\u8BC4\u5BA1\u4E13\u5BB6\u5BA1\u67E5\u4E86\u4F60\u4E4B\u524D\u751F\u6210\u7684 SDD\uFF0C\u53D1\u73B0\u4E86\u4EE5\u4E0B\u95EE\u9898\uFF1A
3093
+
3094
+ ${questions}
3095
+
3096
+ \u8BF7\u4F60\uFF1A
3097
+ 1. \u9010\u4E00\u56DE\u5E94\u6BCF\u4E2A\u95EE\u9898\uFF0C\u5728 SDD \u4E2D\u4FEE\u590D\u6216\u8865\u5145\u5BF9\u5E94\u5185\u5BB9
3098
+ 2. \u5982\u679C\u67D0\u4E2A\u95EE\u9898\u9700\u8981\u67E5\u770B\u4EE3\u7801\u6765\u56DE\u7B54\uFF0C\u8BF7\u4F7F\u7528 Read\u3001Grep\u3001Glob \u7B49\u5DE5\u5177\u67E5\u770B\u9879\u76EE\u4EE3\u7801\u540E\u518D\u4F5C\u7B54
3099
+ 3. \u5BF9\u65E0\u6CD5\u786E\u5B9A\u7684\u95EE\u9898\uFF0C\u7ED9\u51FA\u591A\u79CD\u65B9\u6848\u5E76\u6807\u6CE8\u63A8\u8350\u65B9\u6848
3100
+
3101
+ \u8BF7\u8F93\u51FA\u4FEE\u590D\u540E\u7684\u5B8C\u6574 SDD \u6587\u6863\uFF08\u7EAF Markdown \u6587\u672C\uFF09\uFF0C\u4E0D\u8981\u8F93\u51FA\u5BF9\u6BD4\u8BF4\u660E\uFF0C\u76F4\u63A5\u7ED9\u51FA\u6700\u65B0\u7248\u672C\u3002`;
3102
+ }
3103
+
3104
+ // src/services/BrainstormService.ts
3105
+ var logger12 = logger.child("Brainstorm");
3106
+ function agentConfigToAIConfig(agentCfg, timeoutMs) {
3107
+ return {
3108
+ mode: agentCfg.mode,
3109
+ binary: agentCfg.binary,
3110
+ phaseTimeoutMs: timeoutMs,
3111
+ nvmNodeVersion: agentCfg.nvmNodeVersion,
3112
+ model: agentCfg.model
3113
+ };
3114
+ }
3115
+ var BrainstormService = class {
3116
+ sessions = /* @__PURE__ */ new Map();
3117
+ generatorRunner;
3118
+ reviewerRunner;
3119
+ config;
3120
+ workDir;
3121
+ constructor(config) {
3122
+ this.config = config.brainstorm;
3123
+ this.workDir = config.project.workDir;
3124
+ this.generatorRunner = createAIRunner(
3125
+ agentConfigToAIConfig(this.config.generator, this.config.timeoutMs)
3126
+ );
3127
+ this.reviewerRunner = createAIRunner(
3128
+ agentConfigToAIConfig(this.config.reviewer, this.config.timeoutMs)
3129
+ );
3130
+ }
3131
+ createSession(transcript) {
3132
+ const session = {
3133
+ id: randomUUID(),
3134
+ transcript,
3135
+ currentSdd: "",
3136
+ rounds: [],
3137
+ status: "idle",
3138
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
3139
+ };
3140
+ this.sessions.set(session.id, session);
3141
+ logger12.info("Created brainstorm session", { sessionId: session.id });
3142
+ return session;
3143
+ }
3144
+ getSession(id) {
3145
+ return this.sessions.get(id);
3146
+ }
3147
+ async generate(sessionId, onEvent) {
3148
+ const session = this.requireSession(sessionId);
3149
+ session.status = "generating";
3150
+ logger12.info("Generating SDD", { sessionId });
3151
+ const prompt = buildGeneratePrompt(session.transcript);
3152
+ const result = await this.generatorRunner.run({
3153
+ prompt,
3154
+ workDir: this.workDir,
3155
+ timeoutMs: this.config.timeoutMs,
3156
+ onStreamEvent: (evt) => {
3157
+ onEvent?.({ type: "sdd:chunk", data: evt });
3158
+ }
3159
+ });
3160
+ if (result.success) {
3161
+ session.currentSdd = result.output;
3162
+ session.generatorSessionId = result.sessionId;
3163
+ session.status = "idle";
3164
+ onEvent?.({ type: "sdd:complete", data: { sdd: result.output } });
3165
+ } else {
3166
+ session.status = "error";
3167
+ session.error = result.output;
3168
+ onEvent?.({ type: "error", data: { message: result.output } });
3169
+ }
3170
+ return result;
3171
+ }
3172
+ async review(sessionId, onEvent) {
3173
+ const session = this.requireSession(sessionId);
3174
+ const roundNum = session.rounds.length + 1;
3175
+ session.status = "reviewing";
3176
+ logger12.info("Reviewing SDD", { sessionId, round: roundNum });
3177
+ onEvent?.({ type: "round:start", data: { round: roundNum, phase: "review" }, round: roundNum });
3178
+ const prompt = buildReviewPrompt(session.currentSdd, roundNum);
3179
+ const result = await this.reviewerRunner.run({
3180
+ prompt,
3181
+ workDir: this.workDir,
3182
+ timeoutMs: this.config.timeoutMs,
3183
+ onStreamEvent: (evt) => {
3184
+ onEvent?.({ type: "review:chunk", data: evt, round: roundNum });
3185
+ }
3186
+ });
3187
+ if (result.success) {
3188
+ session.rounds.push({
3189
+ round: roundNum,
3190
+ questions: result.output,
3191
+ refinedSdd: ""
3192
+ });
3193
+ session.status = "idle";
3194
+ onEvent?.({ type: "review:complete", data: { round: roundNum, questions: result.output }, round: roundNum });
3195
+ } else {
3196
+ session.status = "error";
3197
+ session.error = result.output;
3198
+ onEvent?.({ type: "error", data: { message: result.output, round: roundNum }, round: roundNum });
3199
+ }
3200
+ return result;
3201
+ }
3202
+ async refine(sessionId, onEvent) {
3203
+ const session = this.requireSession(sessionId);
3204
+ const currentRound = session.rounds[session.rounds.length - 1];
3205
+ if (!currentRound) {
3206
+ throw new Error("No review round to refine from");
3207
+ }
3208
+ session.status = "refining";
3209
+ logger12.info("Refining SDD", { sessionId, round: currentRound.round });
3210
+ const prompt = buildRefinePrompt(currentRound.questions);
3211
+ const result = await this.generatorRunner.run({
3212
+ prompt,
3213
+ workDir: this.workDir,
3214
+ timeoutMs: this.config.timeoutMs,
3215
+ sessionId: session.generatorSessionId,
3216
+ continueSession: !!session.generatorSessionId,
3217
+ onStreamEvent: (evt) => {
3218
+ onEvent?.({ type: "sdd:chunk", data: evt, round: currentRound.round });
3219
+ }
3220
+ });
3221
+ if (result.success) {
3222
+ currentRound.refinedSdd = result.output;
3223
+ session.currentSdd = result.output;
3224
+ session.generatorSessionId = result.sessionId ?? session.generatorSessionId;
3225
+ session.status = "idle";
3226
+ onEvent?.({
3227
+ type: "round:complete",
3228
+ data: { round: currentRound.round, sdd: result.output },
3229
+ round: currentRound.round
3230
+ });
3231
+ } else {
3232
+ session.status = "error";
3233
+ session.error = result.output;
3234
+ onEvent?.({ type: "error", data: { message: result.output, round: currentRound.round }, round: currentRound.round });
3235
+ }
3236
+ return result;
3237
+ }
3238
+ async autoRefine(sessionId, rounds, onEvent) {
3239
+ const maxRounds = Math.min(rounds ?? this.config.maxRefinementRounds, this.config.maxRefinementRounds);
3240
+ const session = this.requireSession(sessionId);
3241
+ if (!session.currentSdd) {
3242
+ await this.generate(sessionId, onEvent);
3243
+ if (session.status === "error") return;
3244
+ }
3245
+ for (let i = 0; i < maxRounds; i++) {
3246
+ const reviewResult = await this.review(sessionId, onEvent);
3247
+ if (!reviewResult.success) break;
3248
+ const refineResult = await this.refine(sessionId, onEvent);
3249
+ if (!refineResult.success) break;
3250
+ }
3251
+ if (session.status !== "error") {
3252
+ session.status = "done";
3253
+ }
3254
+ }
3255
+ requireSession(id) {
3256
+ const session = this.sessions.get(id);
3257
+ if (!session) {
3258
+ throw new Error(`Brainstorm session not found: ${id}`);
3259
+ }
3260
+ return session;
3261
+ }
3262
+ };
3263
+
3264
+ export {
3265
+ loadConfig,
3266
+ Logger,
3267
+ logger,
3268
+ AGENT_NOTE_MARKER,
3269
+ GongfengClient,
3270
+ GitOperations,
3271
+ BaseAIRunner,
3272
+ ClaudeInternalRunner,
3273
+ CodebuddyRunner,
3274
+ CursorAgentRunner,
3275
+ createAIRunner,
3276
+ CLASSIC_PIPELINE,
3277
+ PLAN_MODE_PIPELINE,
3278
+ resolvePipelineMode,
3279
+ getPipelineDef,
3280
+ collectStateLabels,
3281
+ eventBus,
3282
+ IssueTracker,
3283
+ PlanPersistence,
3284
+ getNoteSyncEnabled,
3285
+ setNoteSyncOverride,
3286
+ isNoteSyncEnabledForIssue,
3287
+ BasePhase,
3288
+ getE2eEnabled,
3289
+ setE2eOverride,
3290
+ validatePhaseRegistry,
3291
+ AsyncMutex,
3292
+ PipelineOrchestrator,
3293
+ BrainstormService
3294
+ };
3295
+ //# sourceMappingURL=chunk-TBIEB3JY.js.map