@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.
- package/bin/issue-auto-finish.js +2 -0
- package/dist/ai-runner/AIRunner.d.ts +27 -0
- package/dist/ai-runner/AIRunner.d.ts.map +1 -0
- package/dist/ai-runner/BaseAIRunner.d.ts +19 -0
- package/dist/ai-runner/BaseAIRunner.d.ts.map +1 -0
- package/dist/ai-runner/ClaudeInternalRunner.d.ts +13 -0
- package/dist/ai-runner/ClaudeInternalRunner.d.ts.map +1 -0
- package/dist/ai-runner/CodebuddyRunner.d.ts +13 -0
- package/dist/ai-runner/CodebuddyRunner.d.ts.map +1 -0
- package/dist/ai-runner/CursorAgentRunner.d.ts +13 -0
- package/dist/ai-runner/CursorAgentRunner.d.ts.map +1 -0
- package/dist/ai-runner/index.d.ts +15 -0
- package/dist/ai-runner/index.d.ts.map +1 -0
- package/dist/chunk-HCHEFK4Z.js +80 -0
- package/dist/chunk-HCHEFK4Z.js.map +1 -0
- package/dist/chunk-I3T573SU.js +153 -0
- package/dist/chunk-I3T573SU.js.map +1 -0
- package/dist/chunk-IDUKWCC2.js +1995 -0
- package/dist/chunk-IDUKWCC2.js.map +1 -0
- package/dist/chunk-OWVT3Z34.js +770 -0
- package/dist/chunk-OWVT3Z34.js.map +1 -0
- package/dist/chunk-RIUI4ROA.js +180 -0
- package/dist/chunk-RIUI4ROA.js.map +1 -0
- package/dist/chunk-TBIEB3JY.js +3295 -0
- package/dist/chunk-TBIEB3JY.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/start.d.ts +5 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/setup/ConfigGenerator.d.ts +44 -0
- package/dist/cli/setup/ConfigGenerator.d.ts.map +1 -0
- package/dist/cli/setup/DependencyChecker.d.ts +21 -0
- package/dist/cli/setup/DependencyChecker.d.ts.map +1 -0
- package/dist/cli.js +73 -0
- package/dist/cli.js.map +1 -0
- package/dist/clients/GongfengClient.d.ts +86 -0
- package/dist/clients/GongfengClient.d.ts.map +1 -0
- package/dist/config.d.ts +92 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/deploy/DevServerManager.d.ts +21 -0
- package/dist/deploy/DevServerManager.d.ts.map +1 -0
- package/dist/deploy/PortAllocator.d.ts +20 -0
- package/dist/deploy/PortAllocator.d.ts.map +1 -0
- package/dist/deploy/index.d.ts +3 -0
- package/dist/deploy/index.d.ts.map +1 -0
- package/dist/doctor-B26Q6JWI.js +33 -0
- package/dist/doctor-B26Q6JWI.js.map +1 -0
- package/dist/e2e/E2eSettings.d.ts +6 -0
- package/dist/e2e/E2eSettings.d.ts.map +1 -0
- package/dist/e2e/ScreenshotCollector.d.ts +11 -0
- package/dist/e2e/ScreenshotCollector.d.ts.map +1 -0
- package/dist/e2e/ScreenshotPublisher.d.ts +16 -0
- package/dist/e2e/ScreenshotPublisher.d.ts.map +1 -0
- package/dist/events/EventBus.d.ts +14 -0
- package/dist/events/EventBus.d.ts.map +1 -0
- package/dist/git/GitOperations.d.ts +30 -0
- package/dist/git/GitOperations.d.ts.map +1 -0
- package/dist/git/WorktreeContext.d.ts +9 -0
- package/dist/git/WorktreeContext.d.ts.map +1 -0
- package/dist/i18n/index.d.ts +8 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/locales/en.d.ts +2 -0
- package/dist/i18n/locales/en.d.ts.map +1 -0
- package/dist/i18n/locales/zh-CN.d.ts +2 -0
- package/dist/i18n/locales/zh-CN.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/init-L3VIWCOV.js +65 -0
- package/dist/init-L3VIWCOV.js.map +1 -0
- package/dist/lib.d.ts +26 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +50 -0
- package/dist/lib.js.map +1 -0
- package/dist/logger.d.ts +15 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/notesync/NoteSyncSettings.d.ts +12 -0
- package/dist/notesync/NoteSyncSettings.d.ts.map +1 -0
- package/dist/orchestrator/PipelineOrchestrator.d.ts +53 -0
- package/dist/orchestrator/PipelineOrchestrator.d.ts.map +1 -0
- package/dist/persistence/PlanPersistence.d.ts +37 -0
- package/dist/persistence/PlanPersistence.d.ts.map +1 -0
- package/dist/phases/AnalysisPhase.d.ts +13 -0
- package/dist/phases/AnalysisPhase.d.ts.map +1 -0
- package/dist/phases/BasePhase.d.ts +44 -0
- package/dist/phases/BasePhase.d.ts.map +1 -0
- package/dist/phases/BuildPhase.d.ts +9 -0
- package/dist/phases/BuildPhase.d.ts.map +1 -0
- package/dist/phases/DesignPhase.d.ts +13 -0
- package/dist/phases/DesignPhase.d.ts.map +1 -0
- package/dist/phases/ImplementPhase.d.ts +9 -0
- package/dist/phases/ImplementPhase.d.ts.map +1 -0
- package/dist/phases/PhaseFactory.d.ts +13 -0
- package/dist/phases/PhaseFactory.d.ts.map +1 -0
- package/dist/phases/PlanPhase.d.ts +14 -0
- package/dist/phases/PlanPhase.d.ts.map +1 -0
- package/dist/phases/VerifyPhase.d.ts +13 -0
- package/dist/phases/VerifyPhase.d.ts.map +1 -0
- package/dist/pipeline/PipelineDefinition.d.ts +40 -0
- package/dist/pipeline/PipelineDefinition.d.ts.map +1 -0
- package/dist/poller/IssuePoller.d.ts +26 -0
- package/dist/poller/IssuePoller.d.ts.map +1 -0
- package/dist/prompts/brainstorm-templates.d.ts +4 -0
- package/dist/prompts/brainstorm-templates.d.ts.map +1 -0
- package/dist/prompts/templates.d.ts +27 -0
- package/dist/prompts/templates.d.ts.map +1 -0
- package/dist/rules/RuleResolver.d.ts +13 -0
- package/dist/rules/RuleResolver.d.ts.map +1 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/run.d.ts +2 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +15 -0
- package/dist/run.js.map +1 -0
- package/dist/services/BrainstormService.d.ts +39 -0
- package/dist/services/BrainstormService.d.ts.map +1 -0
- package/dist/start-TVN4SS6E.js +25 -0
- package/dist/start-TVN4SS6E.js.map +1 -0
- package/dist/supplement/SupplementStore.d.ts +21 -0
- package/dist/supplement/SupplementStore.d.ts.map +1 -0
- package/dist/tracker/IssueState.d.ts +63 -0
- package/dist/tracker/IssueState.d.ts.map +1 -0
- package/dist/tracker/IssueTracker.d.ts +29 -0
- package/dist/tracker/IssueTracker.d.ts.map +1 -0
- package/dist/utils/AsyncMutex.d.ts +14 -0
- package/dist/utils/AsyncMutex.d.ts.map +1 -0
- package/dist/utils/MergeRequestHelper.d.ts +10 -0
- package/dist/utils/MergeRequestHelper.d.ts.map +1 -0
- package/dist/web/AgentLogStore.d.ts +22 -0
- package/dist/web/AgentLogStore.d.ts.map +1 -0
- package/dist/web/WebServer.d.ts +26 -0
- package/dist/web/WebServer.d.ts.map +1 -0
- package/dist/web/routes/api.d.ts +20 -0
- package/dist/web/routes/api.d.ts.map +1 -0
- package/dist/web/routes/brainstorm.d.ts +9 -0
- package/dist/web/routes/brainstorm.d.ts.map +1 -0
- package/dist/web/routes/setup.d.ts +8 -0
- package/dist/web/routes/setup.d.ts.map +1 -0
- package/dist/webhook/CommandExecutor.d.ts +45 -0
- package/dist/webhook/CommandExecutor.d.ts.map +1 -0
- package/dist/webhook/CommandParser.d.ts +24 -0
- package/dist/webhook/CommandParser.d.ts.map +1 -0
- package/dist/webhook/IntentRecognizer.d.ts +35 -0
- package/dist/webhook/IntentRecognizer.d.ts.map +1 -0
- package/dist/webhook/NoteDeduplicator.d.ts +18 -0
- package/dist/webhook/NoteDeduplicator.d.ts.map +1 -0
- package/dist/webhook/WebhookHandler.d.ts +30 -0
- package/dist/webhook/WebhookHandler.d.ts.map +1 -0
- package/dist/webhook/WebhookServer.d.ts +21 -0
- package/dist/webhook/WebhookServer.d.ts.map +1 -0
- package/dist/webhook/index.d.ts +12 -0
- package/dist/webhook/index.d.ts.map +1 -0
- package/package.json +82 -0
- package/src/web/frontend/dist/assets/index-CQdlU9PE.js +65 -0
- package/src/web/frontend/dist/assets/index-CgMEkyZJ.css +1 -0
- 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
|