@xdevops/issue-auto-finish 1.0.2 → 1.0.3

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