@xdevops/issue-auto-finish 1.0.87 → 1.0.89

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 (134) hide show
  1. package/dist/{AIRunnerRegistry-II3WWSFN.js → AIRunnerRegistry-CFDNWSXC.js} +6 -3
  2. package/dist/{LockNote-Z2CLDZNN.js → LockNote-W2JNVMW7.js} +3 -3
  3. package/dist/PtyRunner-NYASBTRP.js +33 -0
  4. package/dist/SdkRunner-U2OTOMZU.js +9 -0
  5. package/dist/ai-runner/AIRunner.d.ts +19 -1
  6. package/dist/ai-runner/AIRunner.d.ts.map +1 -1
  7. package/dist/ai-runner/AIRunnerRegistry.d.ts +8 -0
  8. package/dist/ai-runner/AIRunnerRegistry.d.ts.map +1 -1
  9. package/dist/ai-runner/PlanFileResolver.d.ts +53 -0
  10. package/dist/ai-runner/PlanFileResolver.d.ts.map +1 -0
  11. package/dist/ai-runner/PtyRunner.d.ts +45 -4
  12. package/dist/ai-runner/PtyRunner.d.ts.map +1 -1
  13. package/dist/ai-runner/SdkRunner.d.ts +22 -0
  14. package/dist/ai-runner/SdkRunner.d.ts.map +1 -0
  15. package/dist/ai-runner/index.d.ts +5 -2
  16. package/dist/ai-runner/index.d.ts.map +1 -1
  17. package/dist/ai-runner/sdk/ClaudeCodeSDK.d.ts +37 -0
  18. package/dist/ai-runner/sdk/ClaudeCodeSDK.d.ts.map +1 -0
  19. package/dist/ai-runner/sdk/Stream.d.ts +22 -0
  20. package/dist/ai-runner/sdk/Stream.d.ts.map +1 -0
  21. package/dist/ai-runner/sdk/types.d.ts +146 -0
  22. package/dist/ai-runner/sdk/types.d.ts.map +1 -0
  23. package/dist/{ai-runner-HLA44WI6.js → ai-runner-TOHVJJ76.js} +14 -5
  24. package/dist/{analyze-ZIXNC5GN.js → analyze-DBH4K3J7.js} +8 -6
  25. package/dist/{analyze-ZIXNC5GN.js.map → analyze-DBH4K3J7.js.map} +1 -1
  26. package/dist/{braindump-56WAY2RD.js → braindump-RYI4BGMG.js} +11 -9
  27. package/dist/{braindump-56WAY2RD.js.map → braindump-RYI4BGMG.js.map} +1 -1
  28. package/dist/{chunk-AVGZH64A.js → chunk-2RWGZPNF.js} +4 -1
  29. package/dist/chunk-2RWGZPNF.js.map +1 -0
  30. package/dist/chunk-4XMYOXGZ.js +1153 -0
  31. package/dist/chunk-4XMYOXGZ.js.map +1 -0
  32. package/dist/{chunk-UBQLXQ7I.js → chunk-5JBADEKR.js} +7 -7
  33. package/dist/{chunk-M5C2WILQ.js → chunk-5M5SB6ZA.js} +7 -5
  34. package/dist/{chunk-M5C2WILQ.js.map → chunk-5M5SB6ZA.js.map} +1 -1
  35. package/dist/{chunk-HDFNMVRQ.js → chunk-DVNAH2GV.js} +2 -2
  36. package/dist/{chunk-GXFG4JU6.js → chunk-EU4XFZ2T.js} +2 -2
  37. package/dist/{chunk-NZHKAPU6.js → chunk-FJTZKAJA.js} +9 -3
  38. package/dist/chunk-FJTZKAJA.js.map +1 -0
  39. package/dist/chunk-G7QI5WDI.js +14 -0
  40. package/dist/chunk-G7QI5WDI.js.map +1 -0
  41. package/dist/{chunk-2YQHKXLL.js → chunk-GPZX4DSY.js} +22 -6
  42. package/dist/chunk-GPZX4DSY.js.map +1 -0
  43. package/dist/{chunk-IP3QTP5A.js → chunk-IWSMQXBL.js} +189 -48
  44. package/dist/chunk-IWSMQXBL.js.map +1 -0
  45. package/dist/{chunk-O3WEV5W3.js → chunk-JMACM7AJ.js} +47 -9
  46. package/dist/chunk-JMACM7AJ.js.map +1 -0
  47. package/dist/chunk-MSL7ROVK.js +1 -0
  48. package/dist/{chunk-YCYVNRLF.js → chunk-OBGEEGQ3.js} +61 -19
  49. package/dist/chunk-OBGEEGQ3.js.map +1 -0
  50. package/dist/chunk-R32Q3RGK.js +666 -0
  51. package/dist/chunk-R32Q3RGK.js.map +1 -0
  52. package/dist/{chunk-SAMTXC4A.js → chunk-TFEPHOVE.js} +12 -17
  53. package/dist/chunk-TFEPHOVE.js.map +1 -0
  54. package/dist/{chunk-QZZGIZWC.js → chunk-XSX3PGQW.js} +63 -20
  55. package/dist/chunk-XSX3PGQW.js.map +1 -0
  56. package/dist/{chunk-2MESXJEZ.js → chunk-YNRKPQLS.js} +3 -3
  57. package/dist/cli/setup/PreflightChecker.d.ts +1 -0
  58. package/dist/cli/setup/PreflightChecker.d.ts.map +1 -1
  59. package/dist/cli/setup/env-metadata.d.ts.map +1 -1
  60. package/dist/cli.js +10 -9
  61. package/dist/cli.js.map +1 -1
  62. package/dist/{config-WTRSZLOC.js → config-23TBYFP5.js} +5 -4
  63. package/dist/config-schema.d.ts +6 -0
  64. package/dist/config-schema.d.ts.map +1 -1
  65. package/dist/config.d.ts +6 -0
  66. package/dist/config.d.ts.map +1 -1
  67. package/dist/{doctor-37JNBGDN.js → doctor-ZG3DO7J5.js} +3 -3
  68. package/dist/errors/AIExecutionError.d.ts +3 -0
  69. package/dist/errors/AIExecutionError.d.ts.map +1 -1
  70. package/dist/{errors-S3BWYA4I.js → errors-J3ZRP66W.js} +2 -2
  71. package/dist/events/EventBus.d.ts +1 -1
  72. package/dist/events/EventBus.d.ts.map +1 -1
  73. package/dist/i18n/locales/en.d.ts.map +1 -1
  74. package/dist/i18n/locales/zh-CN.d.ts.map +1 -1
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +16 -14
  77. package/dist/{init-QQDXGTPB.js → init-37DLQ5AJ.js} +9 -8
  78. package/dist/{init-QQDXGTPB.js.map → init-37DLQ5AJ.js.map} +1 -1
  79. package/dist/lib.js +10 -8
  80. package/dist/lib.js.map +1 -1
  81. package/dist/orchestrator/PendingDialogStore.d.ts +12 -0
  82. package/dist/orchestrator/PendingDialogStore.d.ts.map +1 -0
  83. package/dist/orchestrator/steps/FailureHandler.d.ts.map +1 -1
  84. package/dist/orchestrator/steps/PhaseLoopStep.d.ts.map +1 -1
  85. package/dist/persistence/PlanPersistence.d.ts +5 -0
  86. package/dist/persistence/PlanPersistence.d.ts.map +1 -1
  87. package/dist/persistence/TodolistExtractor.d.ts +31 -0
  88. package/dist/persistence/TodolistExtractor.d.ts.map +1 -0
  89. package/dist/phases/BasePhase.d.ts.map +1 -1
  90. package/dist/phases/PhaseOutcome.d.ts +2 -0
  91. package/dist/phases/PhaseOutcome.d.ts.map +1 -1
  92. package/dist/phases/PlanPhase.d.ts.map +1 -1
  93. package/dist/prompts/templates.d.ts +2 -2
  94. package/dist/prompts/templates.d.ts.map +1 -1
  95. package/dist/{restart-BMILTP5X.js → restart-C7QBXT44.js} +9 -8
  96. package/dist/{restart-BMILTP5X.js.map → restart-C7QBXT44.js.map} +1 -1
  97. package/dist/run.js +16 -14
  98. package/dist/run.js.map +1 -1
  99. package/dist/start-66JO56AW.js +16 -0
  100. package/dist/start-66JO56AW.js.map +1 -0
  101. package/dist/tracker/IssueTracker.d.ts +6 -0
  102. package/dist/tracker/IssueTracker.d.ts.map +1 -1
  103. package/dist/web/routes/api.d.ts.map +1 -1
  104. package/package.json +5 -1
  105. package/src/web/frontend/dist/assets/index-DJzC2saL.css +1 -0
  106. package/src/web/frontend/dist/assets/{index-D_oTMuJU.js → index-Mnu8M3ww.js} +57 -57
  107. package/src/web/frontend/dist/index.html +2 -2
  108. package/dist/PtyRunner-6UGI5STW.js +0 -22
  109. package/dist/chunk-2YQHKXLL.js.map +0 -1
  110. package/dist/chunk-AVGZH64A.js.map +0 -1
  111. package/dist/chunk-IP3QTP5A.js.map +0 -1
  112. package/dist/chunk-NZHKAPU6.js.map +0 -1
  113. package/dist/chunk-O3WEV5W3.js.map +0 -1
  114. package/dist/chunk-QZZGIZWC.js.map +0 -1
  115. package/dist/chunk-SAMTXC4A.js.map +0 -1
  116. package/dist/chunk-U237JSLB.js +0 -1
  117. package/dist/chunk-U6GWFTKA.js +0 -657
  118. package/dist/chunk-U6GWFTKA.js.map +0 -1
  119. package/dist/chunk-YCYVNRLF.js.map +0 -1
  120. package/dist/start-6QRW6IJI.js +0 -15
  121. package/src/web/frontend/dist/assets/index-COYziOhv.css +0 -1
  122. /package/dist/{AIRunnerRegistry-II3WWSFN.js.map → AIRunnerRegistry-CFDNWSXC.js.map} +0 -0
  123. /package/dist/{LockNote-Z2CLDZNN.js.map → LockNote-W2JNVMW7.js.map} +0 -0
  124. /package/dist/{PtyRunner-6UGI5STW.js.map → PtyRunner-NYASBTRP.js.map} +0 -0
  125. /package/dist/{ai-runner-HLA44WI6.js.map → SdkRunner-U2OTOMZU.js.map} +0 -0
  126. /package/dist/{chunk-U237JSLB.js.map → ai-runner-TOHVJJ76.js.map} +0 -0
  127. /package/dist/{chunk-UBQLXQ7I.js.map → chunk-5JBADEKR.js.map} +0 -0
  128. /package/dist/{chunk-HDFNMVRQ.js.map → chunk-DVNAH2GV.js.map} +0 -0
  129. /package/dist/{chunk-GXFG4JU6.js.map → chunk-EU4XFZ2T.js.map} +0 -0
  130. /package/dist/{config-WTRSZLOC.js.map → chunk-MSL7ROVK.js.map} +0 -0
  131. /package/dist/{chunk-2MESXJEZ.js.map → chunk-YNRKPQLS.js.map} +0 -0
  132. /package/dist/{errors-S3BWYA4I.js.map → config-23TBYFP5.js.map} +0 -0
  133. /package/dist/{doctor-37JNBGDN.js.map → doctor-ZG3DO7J5.js.map} +0 -0
  134. /package/dist/{start-6QRW6IJI.js.map → errors-J3ZRP66W.js.map} +0 -0
@@ -0,0 +1,1153 @@
1
+ import {
2
+ getPtyProfile,
3
+ getRegistryEntry,
4
+ getRunnerCapabilities,
5
+ resolveModelForRunner
6
+ } from "./chunk-TFEPHOVE.js";
7
+ import {
8
+ isShuttingDown
9
+ } from "./chunk-G7QI5WDI.js";
10
+ import {
11
+ logger
12
+ } from "./chunk-GF2RRYHB.js";
13
+
14
+ // src/ai-runner/PtyRunner.ts
15
+ import fs2 from "fs";
16
+ import path2 from "path";
17
+
18
+ // src/ai-runner/PlanFileResolver.ts
19
+ import fs from "fs";
20
+ import path from "path";
21
+ import os from "os";
22
+ var logger2 = logger.child("PlanFileResolver");
23
+ var PLAN_DIRS = {
24
+ "claude-internal": path.join(os.homedir(), ".claude-internal", "plans"),
25
+ "codebuddy": path.join(os.homedir(), ".codebuddy", "plans")
26
+ };
27
+ var PlanFileResolver = class _PlanFileResolver {
28
+ plansDir;
29
+ beforeFiles = /* @__PURE__ */ new Map();
30
+ constructor(plansDir) {
31
+ this.plansDir = plansDir ?? path.join(os.homedir(), ".claude-internal", "plans");
32
+ }
33
+ /** Create a resolver for a specific runner mode (e.g. 'claude-internal', 'codebuddy'). */
34
+ static forRunner(agentMode) {
35
+ const dir = PLAN_DIRS[agentMode];
36
+ return new _PlanFileResolver(dir);
37
+ }
38
+ /** Take a snapshot of existing plan files before the plan phase starts. */
39
+ takeBeforeSnapshot() {
40
+ this.beforeFiles = this.listFiles();
41
+ logger2.info("Plan file snapshot taken", {
42
+ dir: this.plansDir,
43
+ fileCount: this.beforeFiles.size
44
+ });
45
+ }
46
+ /**
47
+ * Check if any new or modified plan files exist since the before-snapshot.
48
+ * Used as an artifact gate by detectCompletion to prevent premature
49
+ * completion when the agent is still exploring/thinking.
50
+ */
51
+ hasNewOrModifiedFiles() {
52
+ const afterFiles = this.listFiles();
53
+ for (const [filePath, mtime] of afterFiles) {
54
+ const beforeMtime = this.beforeFiles.get(filePath);
55
+ if (beforeMtime === void 0 || mtime > beforeMtime) return true;
56
+ }
57
+ return false;
58
+ }
59
+ /**
60
+ * After plan phase completes, find the newly created plan file.
61
+ *
62
+ * Strategy:
63
+ * 1. Diff against beforeSnapshot to find new files
64
+ * 2. Sort candidates by mtime (newest first)
65
+ * 3. If contentHint provided, prefer the file whose content matches
66
+ * 4. Return the best candidate
67
+ *
68
+ * @param contentHint - Optional string (e.g. issue IID or title) to validate content
69
+ */
70
+ resolve(contentHint) {
71
+ if (!fs.existsSync(this.plansDir)) {
72
+ logger2.warn("Plans directory does not exist", { dir: this.plansDir });
73
+ return null;
74
+ }
75
+ const afterFiles = this.listFiles();
76
+ const candidates = this.findNewFiles(afterFiles);
77
+ if (candidates.length === 0) {
78
+ logger2.warn("No new plan files found after plan phase", {
79
+ dir: this.plansDir,
80
+ beforeCount: this.beforeFiles.size,
81
+ afterCount: afterFiles.size
82
+ });
83
+ return this.fallbackByMtime(afterFiles, contentHint);
84
+ }
85
+ if (candidates.length === 1) {
86
+ return this.readCandidate(candidates[0].path, candidates[0].mtime);
87
+ }
88
+ if (contentHint) {
89
+ const matched = this.matchByContent(candidates, contentHint);
90
+ if (matched) return matched;
91
+ }
92
+ candidates.sort((a, b) => b.mtime - a.mtime);
93
+ logger2.info("Multiple new plan files found, using most recent", {
94
+ count: candidates.length,
95
+ selected: path.basename(candidates[0].path)
96
+ });
97
+ return this.readCandidate(candidates[0].path, candidates[0].mtime);
98
+ }
99
+ /**
100
+ * Build a content hint string from issue metadata for content-based matching.
101
+ */
102
+ static buildContentHint(issueIid, issueTitle) {
103
+ const parts = [`#${issueIid}`, `${issueIid}`];
104
+ if (issueTitle) parts.push(issueTitle);
105
+ return parts.join("|");
106
+ }
107
+ listFiles() {
108
+ const result = /* @__PURE__ */ new Map();
109
+ if (!fs.existsSync(this.plansDir)) return result;
110
+ try {
111
+ const entries = fs.readdirSync(this.plansDir);
112
+ for (const entry of entries) {
113
+ if (!entry.endsWith(".md")) continue;
114
+ const fullPath = path.join(this.plansDir, entry);
115
+ try {
116
+ const stat = fs.statSync(fullPath);
117
+ if (stat.isFile()) {
118
+ result.set(fullPath, stat.mtimeMs);
119
+ }
120
+ } catch {
121
+ }
122
+ }
123
+ } catch (err) {
124
+ logger2.warn("Failed to list plans directory", { dir: this.plansDir, err });
125
+ }
126
+ return result;
127
+ }
128
+ findNewFiles(afterFiles) {
129
+ const candidates = [];
130
+ for (const [filePath, mtime] of afterFiles) {
131
+ if (!this.beforeFiles.has(filePath)) {
132
+ candidates.push({ path: filePath, mtime });
133
+ }
134
+ }
135
+ return candidates;
136
+ }
137
+ /**
138
+ * Fallback: if no new files found (rare case — plan might have overwritten
139
+ * an existing file), find the most recently modified file.
140
+ */
141
+ fallbackByMtime(afterFiles, contentHint) {
142
+ if (afterFiles.size === 0) return null;
143
+ const sorted = [...afterFiles.entries()].sort((a, b) => b[1] - a[1]);
144
+ if (contentHint) {
145
+ for (const [filePath] of sorted.slice(0, 5)) {
146
+ try {
147
+ const content = fs.readFileSync(filePath, "utf-8");
148
+ if (this.contentMatches(content, contentHint)) {
149
+ logger2.info("Fallback: found plan file by content match", {
150
+ file: path.basename(filePath)
151
+ });
152
+ return { sourcePath: filePath, content };
153
+ }
154
+ } catch {
155
+ continue;
156
+ }
157
+ }
158
+ }
159
+ const [newestPath, newestMtime] = sorted[0];
160
+ logger2.info("Fallback: using most recently modified plan file", {
161
+ file: path.basename(newestPath),
162
+ ageMs: Date.now() - newestMtime
163
+ });
164
+ return this.readCandidate(newestPath, newestMtime);
165
+ }
166
+ matchByContent(candidates, contentHint) {
167
+ for (const candidate of candidates) {
168
+ try {
169
+ const content = fs.readFileSync(candidate.path, "utf-8");
170
+ if (this.contentMatches(content, contentHint)) {
171
+ logger2.info("Plan file matched by content", {
172
+ file: path.basename(candidate.path),
173
+ hint: contentHint.slice(0, 50)
174
+ });
175
+ return { sourcePath: candidate.path, content };
176
+ }
177
+ } catch {
178
+ continue;
179
+ }
180
+ }
181
+ return null;
182
+ }
183
+ contentMatches(content, hint) {
184
+ const parts = hint.split("|");
185
+ return parts.some((part) => content.includes(part));
186
+ }
187
+ readCandidate(filePath, mtime) {
188
+ try {
189
+ const content = fs.readFileSync(filePath, "utf-8");
190
+ logger2.info("Resolved plan file", {
191
+ file: path.basename(filePath),
192
+ size: content.length,
193
+ ageMs: Date.now() - mtime
194
+ });
195
+ return { sourcePath: filePath, content };
196
+ } catch (err) {
197
+ logger2.error("Failed to read plan file", { filePath, err });
198
+ return null;
199
+ }
200
+ }
201
+ };
202
+
203
+ // src/ai-runner/PtyRunner.ts
204
+ var logger3 = logger.child("PtyRunner");
205
+ var ANSI_RE = /\x1b\[[?><=]*[0-9;]*[a-zA-Z~]|\x1b\][^\x07]*\x07|\x1b\(B/g;
206
+ function stripAnsi(str) {
207
+ return str.replace(ANSI_RE, "");
208
+ }
209
+ function extractIidFromPath(workDir) {
210
+ const match = workDir.match(/issue-(\d+)/);
211
+ return match ? parseInt(match[1], 10) : void 0;
212
+ }
213
+ function isIdlePrompt(stripped) {
214
+ const lines = stripped.split("\n").filter((l) => l.trim());
215
+ if (lines.length === 0) return false;
216
+ const last = lines[lines.length - 1].trim();
217
+ if (/^[❯>$%]\s*$/.test(last) || /[❯>]\s*$/.test(last)) return true;
218
+ if (lines.some((l) => {
219
+ const t = l.trim();
220
+ if (!/(?:^|\s)❯/.test(t)) return false;
221
+ if (/❯\s*$/.test(t)) return true;
222
+ return !/❯\s+[A-Za-z]{3,}/.test(t);
223
+ })) return true;
224
+ if (lines.some((l) => /^>/.test(l.trim())) && /⏵/.test(stripped)) return true;
225
+ if (/→/.test(stripped) && /\/\s*commands/.test(stripped) && !/ctrl\+c\s*to\s*stop/i.test(stripped)) {
226
+ return true;
227
+ }
228
+ return false;
229
+ }
230
+ var SPINNER_CHARS = "\u2736\u273B\u273D\u2722\u273E\u25C6\u2756\u25CF\u25D0\u25D1\u25D2\u25D3\u25CB\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F\u2802\u2812\u2810\u2808\u2820\u2824\xB7*\u2026\u22EF\u2B21\u2B22";
231
+ var SPINNER_RE = new RegExp(`^[${SPINNER_CHARS}\\s]+$`);
232
+ var SPINNER_FRAGMENT_RE = new RegExp(`^[${SPINNER_CHARS}][a-zA-Z]{0,3}$`);
233
+ var STATUS_BAR_RE = /bypass permissions|shift\+tab|ctrl\+[a-z].*(?:to |edit)|to interrupt|to cycle|⏵.*(?:bypass|permission)/;
234
+ var THINKING_RE = /^[^\w]*\w[\w-]*…\s*$/;
235
+ var SEPARATOR_RE = /^[─━═-]{10,}$/;
236
+ var EFFORT_RE = /^[◐◑◒◓]\s+(?:low|medium|high)\s+·\s+\/effort$/;
237
+ var CLAUDE_STATUS_BAR_RE = new RegExp(`^[${SPINNER_CHARS}]\\s*\\d+[smh]`);
238
+ var CURSOR_ARTIFACT_RE = /^\d{1,3}$/;
239
+ var CURSOR_GENERATING_RE = /^[⬡⬢]\s*Generating/;
240
+ var CURSOR_FOOTER_RE = /^\/\s*commands\s*·\s*@\s*files\s*·\s*!\s*shell$/;
241
+ var CURSOR_STATUS_RE = /^▶︎?\s*Auto-run/;
242
+ var CURSOR_MODEL_RE = /^(?:Opus|Claude|GPT|Gemini|当前使用的模型)\s*/;
243
+ var BOX_DRAWING_RE = /^[┌┐└┘│─┬┴├┤┼╭╮╰╯═║╔╗╚╝╠╣╦╩╬\s]*$/;
244
+ var QUEUED_MSG_RE = /Press\s*up\s*to\s*edit\s*queued\s*messages/;
245
+ var STOP_HOOK_RE = /^[^\w]*\w[\w-]*…\s*\(.*\)\s*$/;
246
+ var WORKED_SUMMARY_RE = new RegExp(`^[${SPINNER_CHARS}]\\s*(?:\\w+\\s+for|\u5DE5\u4F5C\u4E86)\\s+\\d+[smh]`, "m");
247
+ var CLAUDE_BANNER_RE = /^[▐▛▜▌▝▘█\s]+$/;
248
+ var CLAUDE_BANNER_INFO_RE = /^[▐▛▜▌▝▘█]+\s+(?:Claude|Opus|Gemini|GPT|Sonnet|Haiku)/;
249
+ var TRUST_DIALOG_RE = /trust\s*(?:the\s*files\s*in\s*)?this\s*(?:folder|workspace)|I\s*trust\s*this/i;
250
+ var PERMISSION_DIALOG_RE = /Do\s*you\s*want\s*to\s*proceed\s*\?|(?:❯|>)\s*\d+\.\s*Yes\b|Allow\s+(?:this|the)\s+(?:action|command)\s*\?/i;
251
+ var PLAN_CONFIRM_RE = /written\s*up\s*a\s*plan|ready\s*to\s*execute.*(?:Would|proceed)|Yes,?\s*and\s*bypass\s*permissions/i;
252
+ var NUMBERED_OPTION_RE = /(?:❯|>)?\s*(\d+)\.\s*(.+)/;
253
+ var INTERACTIVE_NAV_HINT_RE = /Enter\s*to\s*select|↑.*↓.*navigate|Esc\s*to\s*cancel/i;
254
+ var CHECKBOX_HEADER_RE = /☐/;
255
+ var HIGHLIGHTED_OPTION_RE = /❯\s*\d+\./;
256
+ function parseInteractiveDialog(stripped) {
257
+ const lines = stripped.split(/[\r\n]+/).map((l) => l.trim()).filter(Boolean);
258
+ const options = [];
259
+ const questionParts = [];
260
+ let optionsStarted = false;
261
+ for (const line of lines) {
262
+ if (isTuiNoise(line)) continue;
263
+ if (INTERACTIVE_NAV_HINT_RE.test(line)) continue;
264
+ const m = NUMBERED_OPTION_RE.exec(line);
265
+ if (m) {
266
+ const label = m[2].trim();
267
+ if (label.length >= 2 && !/^\d+$/.test(label)) {
268
+ if (m.index > 0 && !optionsStarted) {
269
+ const prefix = line.slice(0, m.index).replace(/[─━═╌☐]+/g, " ").trim();
270
+ if (prefix.length > 0) questionParts.push(prefix);
271
+ }
272
+ optionsStarted = true;
273
+ options.push({ index: parseInt(m[1], 10), label });
274
+ }
275
+ } else if (!optionsStarted) {
276
+ if (!/^[❯>$%]\s*$/.test(line) && !/^[─━═╌]{4,}$/.test(line)) {
277
+ questionParts.push(line);
278
+ }
279
+ }
280
+ }
281
+ const minRequired = INTERACTIVE_NAV_HINT_RE.test(stripped) ? 1 : 2;
282
+ if (options.length < minRequired) return null;
283
+ return { question: questionParts.join(" ").trim(), options };
284
+ }
285
+ function getDialogConfidence(stripped) {
286
+ if (INTERACTIVE_NAV_HINT_RE.test(stripped)) return "high";
287
+ if (HIGHLIGHTED_OPTION_RE.test(stripped)) return "high";
288
+ if (CHECKBOX_HEADER_RE.test(stripped)) return "high";
289
+ return "low";
290
+ }
291
+ function isInteractiveDialog(stripped) {
292
+ if (PLAN_CONFIRM_RE.test(stripped)) return false;
293
+ if (PERMISSION_DIALOG_RE.test(stripped)) return false;
294
+ if (parseInteractiveDialog(stripped) !== null) return true;
295
+ if (INTERACTIVE_NAV_HINT_RE.test(stripped)) {
296
+ const lines = stripped.split(/[\r\n]+/).map((l) => l.trim()).filter(Boolean);
297
+ return lines.some((l) => !isTuiNoise(l) && NUMBERED_OPTION_RE.test(l));
298
+ }
299
+ return false;
300
+ }
301
+ function containsActiveWork(stripped) {
302
+ const lines = stripped.split(/[\r\n]+/).filter((l) => l.trim());
303
+ let inTipBlock = false;
304
+ return lines.some((line) => {
305
+ const t = line.trim();
306
+ if (t.length === 0) return false;
307
+ if (/Tip:/i.test(t)) {
308
+ inTipBlock = true;
309
+ return false;
310
+ }
311
+ if (inTipBlock) return false;
312
+ if (/ctrl\+o to expand/i.test(t)) return false;
313
+ if (/^⎿/.test(t)) return false;
314
+ if (/^[❯>$%]\s*$/.test(t) || /[❯>]\s*$/.test(t)) return false;
315
+ if (/^[│║┃]/.test(t) && /[│║┃]$/.test(t)) return false;
316
+ if (isTuiNoise(t)) return false;
317
+ return true;
318
+ });
319
+ }
320
+ function isTuiNoise(line) {
321
+ const t = line.replace(/\r/g, "").trim();
322
+ if (t.length === 0) return true;
323
+ if (SPINNER_RE.test(t)) return true;
324
+ if (t.length <= 4 && SPINNER_FRAGMENT_RE.test(t)) return true;
325
+ if (STATUS_BAR_RE.test(t)) return true;
326
+ if (THINKING_RE.test(t)) return true;
327
+ if (SEPARATOR_RE.test(t)) return true;
328
+ if (EFFORT_RE.test(t)) return true;
329
+ if (CLAUDE_STATUS_BAR_RE.test(t)) return true;
330
+ if (QUEUED_MSG_RE.test(t)) return true;
331
+ if (CURSOR_ARTIFACT_RE.test(t)) return true;
332
+ if (CURSOR_GENERATING_RE.test(t)) return true;
333
+ if (CURSOR_FOOTER_RE.test(t)) return true;
334
+ if (CURSOR_STATUS_RE.test(t)) return true;
335
+ if (CURSOR_MODEL_RE.test(t)) return true;
336
+ if (BOX_DRAWING_RE.test(t) && t.length > 1) return true;
337
+ if (STOP_HOOK_RE.test(t)) return true;
338
+ if (WORKED_SUMMARY_RE.test(t)) return true;
339
+ if (CLAUDE_BANNER_RE.test(t) && t.length > 1) return true;
340
+ if (CLAUDE_BANNER_INFO_RE.test(t)) return true;
341
+ return false;
342
+ }
343
+ var PtyRunner = class {
344
+ constructor(nvmNodeVersion, terminalManager, defaultAgentMode, phaseAgentMap, globalModel, idleDetectMs = 3e4) {
345
+ this.nvmNodeVersion = nvmNodeVersion;
346
+ this.terminalManager = terminalManager;
347
+ this.defaultAgentMode = defaultAgentMode;
348
+ this.phaseAgentMap = phaseAgentMap;
349
+ this.globalModel = globalModel;
350
+ this.idleDetectMs = idleDetectMs;
351
+ }
352
+ /** workDir → active session info (sessionId + current agent type) */
353
+ sessions = /* @__PURE__ */ new Map();
354
+ /** Sessions that were forcefully killed (via killByWorkDir/killAll). Checked by
355
+ * detectCompletion's onExit handler to report failure instead of success. */
356
+ killedSessions = /* @__PURE__ */ new Set();
357
+ // ---- AIRunner interface ---------------------------------------------------
358
+ async run(options) {
359
+ if (isShuttingDown()) {
360
+ logger3.warn("PtyRunner skipped \u2014 service is shutting down");
361
+ return { success: false, output: "Service shutting down", exitCode: null };
362
+ }
363
+ const { prompt, workDir, timeoutMs, onStreamEvent, phaseName } = options;
364
+ const agentMode = this.resolveAgentForPhase(phaseName);
365
+ const startMode = options.mode;
366
+ const continueSession = options.continueSession ?? false;
367
+ logger3.info("PtyRunner.run()", { workDir, timeoutMs, phaseName, agentMode, continueSession });
368
+ const { sessionId, isNew } = this.ensureSession(workDir, agentMode, startMode);
369
+ if (isNew) {
370
+ logger3.info("Waiting for AI agent prompt (new session)", { sessionId, phaseName });
371
+ await this.waitForPrompt(sessionId, 3e5);
372
+ } else if (continueSession) {
373
+ logger3.info("Waiting for AI agent prompt (continue-session)", { sessionId, phaseName });
374
+ await this.waitForPrompt(sessionId, 3e4);
375
+ } else {
376
+ logger3.info("Waiting for AI agent prompt (reused session)", { sessionId, phaseName });
377
+ await this.waitForPrompt(sessionId, 1e4);
378
+ }
379
+ if (startMode === "plan" && this.shouldUseNativePlan(agentMode)) {
380
+ return this.runNativePlanMode(sessionId, isNew, options, agentMode, workDir);
381
+ }
382
+ if (continueSession && !isNew) {
383
+ logger3.info("Continue-session mode: attaching detectCompletion (no /clear)", { sessionId });
384
+ const result2 = await this.detectCompletion(sessionId, options, onStreamEvent);
385
+ logger3.info("PtyRunner continue-session completed", {
386
+ workDir,
387
+ agentMode,
388
+ phaseName,
389
+ timedOut: result2.timedOut,
390
+ outputLength: result2.output.length
391
+ });
392
+ return this.buildRunResult(result2, sessionId);
393
+ }
394
+ await this.ensurePlanMode(sessionId, agentMode, startMode === "plan", workDir);
395
+ await this.writeCommand(sessionId, "/clear", agentMode);
396
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
397
+ const promptFile = this.writePromptFile(workDir, prompt);
398
+ const instruction = `Please read and follow all instructions in ${promptFile}`;
399
+ await this.writeCommand(sessionId, instruction, agentMode);
400
+ const result = await this.detectCompletion(sessionId, options, onStreamEvent);
401
+ logger3.info("PtyRunner phase completed", {
402
+ workDir,
403
+ agentMode,
404
+ phaseName,
405
+ timedOut: result.timedOut,
406
+ outputLength: result.output.length
407
+ });
408
+ return this.buildRunResult(result, sessionId);
409
+ }
410
+ killAll() {
411
+ for (const [, info] of this.sessions) {
412
+ this.killedSessions.add(info.sessionId);
413
+ this.terminalManager.destroy(info.sessionId);
414
+ }
415
+ this.sessions.clear();
416
+ logger3.info("PtyRunner: all managed sessions destroyed");
417
+ }
418
+ killByWorkDir(targetWorkDir) {
419
+ const info = this.sessions.get(targetWorkDir);
420
+ if (!info) return 0;
421
+ this.killedSessions.add(info.sessionId);
422
+ this.terminalManager.destroy(info.sessionId);
423
+ this.sessions.delete(targetWorkDir);
424
+ return 1;
425
+ }
426
+ interruptByWorkDir(targetWorkDir) {
427
+ const info = this.sessions.get(targetWorkDir);
428
+ if (!info) return false;
429
+ const session = this.terminalManager.get(info.sessionId);
430
+ if (!session) {
431
+ this.sessions.delete(targetWorkDir);
432
+ return false;
433
+ }
434
+ this.terminalManager.write(info.sessionId, "");
435
+ logger3.info("Interrupted PTY session for retry", {
436
+ workDir: targetWorkDir,
437
+ sessionId: info.sessionId
438
+ });
439
+ return true;
440
+ }
441
+ // ---- Agent resolution ----------------------------------------------------
442
+ /** Get the Enter key sequence for the given agent mode */
443
+ getEnterKey(agentMode) {
444
+ const profile = getPtyProfile(agentMode);
445
+ return profile?.enterKey ?? "\r";
446
+ }
447
+ /**
448
+ * Write a command to the PTY and press Enter.
449
+ *
450
+ * Some agents (codebuddy) use input coalescing in their TUI: when text and
451
+ * Enter arrive in the same PTY write, the Enter is treated as a newline
452
+ * character (paste) instead of triggering message submission. For these
453
+ * agents (PtyProfile.separateEnter=true), text and Enter are sent in
454
+ * separate writes with a 150ms gap so the Enter key is recognized as
455
+ * a standalone key press.
456
+ */
457
+ async writeCommand(sessionId, text, agentMode) {
458
+ const enterKey = this.getEnterKey(agentMode);
459
+ const profile = getPtyProfile(agentMode);
460
+ if (profile?.separateEnter) {
461
+ this.terminalManager.write(sessionId, text);
462
+ await new Promise((resolve) => setTimeout(resolve, 150));
463
+ this.terminalManager.write(sessionId, enterKey);
464
+ } else {
465
+ this.terminalManager.write(sessionId, text + enterKey);
466
+ }
467
+ }
468
+ /** Resolve agent mode for a given phase (fallback to default) */
469
+ resolveAgentForPhase(phaseName) {
470
+ if (phaseName && this.phaseAgentMap[phaseName]) {
471
+ return this.phaseAgentMap[phaseName];
472
+ }
473
+ return this.defaultAgentMode;
474
+ }
475
+ /** Look up PtyProfile from registry + resolve agent-specific binary and model */
476
+ resolveProfileAndModel(agentMode) {
477
+ const profile = getPtyProfile(agentMode);
478
+ if (!profile) {
479
+ throw new Error(
480
+ `Agent "${agentMode}" has no PtyProfile \u2014 not supported in PTY mode. Compatible agents: claude-internal, codebuddy, cursor-agent`
481
+ );
482
+ }
483
+ const entry = getRegistryEntry(agentMode);
484
+ const binary = entry && process.env[entry.binaryEnvKey] || entry?.defaultBinary || agentMode;
485
+ const model = this.globalModel ? resolveModelForRunner(agentMode, this.globalModel) : void 0;
486
+ return { profile, model, binary };
487
+ }
488
+ // ---- Session management ---------------------------------------------------
489
+ ensureSession(workDir, agentMode, startMode) {
490
+ const existing = this.sessions.get(workDir);
491
+ if (existing && existing.agentMode !== agentMode) {
492
+ logger3.info("Agent switched, destroying old PTY session", {
493
+ workDir,
494
+ oldAgent: existing.agentMode,
495
+ newAgent: agentMode,
496
+ sessionId: existing.sessionId
497
+ });
498
+ this.terminalManager.destroy(existing.sessionId);
499
+ this.sessions.delete(workDir);
500
+ }
501
+ if (existing && existing.agentMode === agentMode) {
502
+ if (this.terminalManager.get(existing.sessionId)) {
503
+ logger3.info("Reusing existing PTY session (same agent)", {
504
+ workDir,
505
+ agentMode,
506
+ sessionId: existing.sessionId
507
+ });
508
+ return { sessionId: existing.sessionId, isNew: false };
509
+ }
510
+ this.sessions.delete(workDir);
511
+ }
512
+ const orphan = this.terminalManager.findByWorkDir(workDir);
513
+ if (orphan) {
514
+ logger3.info("Destroying orphaned PTY session on workDir", {
515
+ workDir,
516
+ sessionId: orphan.id
517
+ });
518
+ this.terminalManager.destroy(orphan.id);
519
+ }
520
+ const { profile, model, binary } = this.resolveProfileAndModel(agentMode);
521
+ const args = profile.buildPtyArgs({ model, startMode });
522
+ const issueIid = extractIidFromPath(workDir);
523
+ const info = this.terminalManager.create({
524
+ workDir,
525
+ issueIid,
526
+ command: binary,
527
+ args,
528
+ managed: true
529
+ });
530
+ this.sessions.set(workDir, {
531
+ sessionId: info.id,
532
+ agentMode,
533
+ // Always set initial mode to defaultModeName — CLI args always use bypass
534
+ // (startMode is applied at runtime via Shift+Tab in ensurePlanMode).
535
+ currentMode: profile.defaultModeName ?? "bypass",
536
+ startedWithMode: startMode
537
+ });
538
+ logger3.info("Created new PTY session", {
539
+ workDir,
540
+ agentMode,
541
+ binary,
542
+ args,
543
+ sessionId: info.id,
544
+ pid: info.pid,
545
+ issueIid
546
+ });
547
+ return { sessionId: info.id, isNew: true };
548
+ }
549
+ // ---- Prompt delivery ------------------------------------------------------
550
+ /**
551
+ * Wait for the AI agent to show its idle prompt (❯ or >), indicating
552
+ * it is ready to accept input. Used before sending /clear and instructions
553
+ * to prevent commands from arriving before the agent is initialized.
554
+ */
555
+ waitForPrompt(sessionId, timeoutMs = 6e4) {
556
+ return new Promise((resolve) => {
557
+ let promptSeen = false;
558
+ let trustDialogHandled = false;
559
+ let stabilityTimer;
560
+ const STABILITY_MS = 3e3;
561
+ const TUI_READY_RE = /bypass\s*permissions|shift\+?\s*tab\s*to\s*cycle/i;
562
+ let tuiReady = false;
563
+ let bannerSeen = false;
564
+ let silenceTimer;
565
+ const SILENCE_READY_MS = 8e3;
566
+ const timer = setTimeout(() => {
567
+ if (stabilityTimer) clearTimeout(stabilityTimer);
568
+ if (silenceTimer) clearTimeout(silenceTimer);
569
+ subscription.dispose();
570
+ logger3.warn("Timed out waiting for AI agent prompt", { sessionId, timeoutMs });
571
+ resolve();
572
+ }, timeoutMs);
573
+ const done = (reason) => {
574
+ clearTimeout(timer);
575
+ if (stabilityTimer) clearTimeout(stabilityTimer);
576
+ if (silenceTimer) clearTimeout(silenceTimer);
577
+ subscription.dispose();
578
+ logger3.info("AI agent prompt detected", { sessionId, reason });
579
+ resolve();
580
+ };
581
+ const resetSilenceTimer = () => {
582
+ if (!bannerSeen) return;
583
+ if (silenceTimer) clearTimeout(silenceTimer);
584
+ silenceTimer = setTimeout(() => {
585
+ if (promptSeen) return;
586
+ logger3.info("Banner shown and PTY silent \u2014 treating agent as ready", { sessionId });
587
+ done("silence-after-banner");
588
+ }, SILENCE_READY_MS);
589
+ };
590
+ const subscription = this.terminalManager.onData(sessionId, (data) => {
591
+ const stripped = stripAnsi(data);
592
+ const nonEmptyLines = stripped.split("\n").filter((l) => l.trim());
593
+ const isNoise = nonEmptyLines.length === 0 || nonEmptyLines.every((l) => isTuiNoise(l));
594
+ if (!bannerSeen && /Claude\s*Code/i.test(stripped)) {
595
+ bannerSeen = true;
596
+ }
597
+ resetSilenceTimer();
598
+ if (!trustDialogHandled && TRUST_DIALOG_RE.test(stripped)) {
599
+ trustDialogHandled = true;
600
+ logger3.info("Trust dialog detected, auto-confirming", { sessionId });
601
+ setTimeout(() => this.terminalManager.write(sessionId, "\r"), 500);
602
+ }
603
+ if (PERMISSION_DIALOG_RE.test(stripped)) {
604
+ logger3.info("Permission dialog detected in waitForPrompt, auto-confirming", { sessionId });
605
+ setTimeout(() => this.terminalManager.write(sessionId, "\r"), 500);
606
+ }
607
+ if (isIdlePrompt(stripped)) {
608
+ promptSeen = true;
609
+ }
610
+ if (!tuiReady && TUI_READY_RE.test(stripped)) {
611
+ tuiReady = true;
612
+ }
613
+ if (promptSeen || tuiReady) {
614
+ if (stabilityTimer && isNoise) {
615
+ } else {
616
+ if (stabilityTimer) clearTimeout(stabilityTimer);
617
+ const reason = promptSeen ? "idle-prompt-stable" : "status-bar-ready";
618
+ stabilityTimer = setTimeout(() => done(reason), STABILITY_MS);
619
+ }
620
+ }
621
+ });
622
+ });
623
+ }
624
+ // ---- Interactive mode switching --------------------------------------------
625
+ /**
626
+ * Switch the PTY session to (or away from) plan mode by pressing the mode
627
+ * cycle key (Shift+Tab) until the target mode is detected in the output.
628
+ *
629
+ * This is a no-op when the agent has no modeCycleKey configured, or the
630
+ * session is already in the desired mode.
631
+ */
632
+ async ensurePlanMode(sessionId, agentMode, wantPlan, workDir) {
633
+ const profile = getPtyProfile(agentMode);
634
+ if (!profile?.modeCycleKey || !profile.detectMode || !profile.planModeName) return;
635
+ const session = this.sessions.get(workDir);
636
+ if (!session) return;
637
+ const targetMode = wantPlan ? profile.planModeName : profile.defaultModeName ?? "bypass";
638
+ if (session.currentMode === targetMode) {
639
+ logger3.info("PTY already in target mode", { sessionId, targetMode });
640
+ return;
641
+ }
642
+ const MAX_ATTEMPTS = 5;
643
+ for (let i = 0; i < MAX_ATTEMPTS; i++) {
644
+ const outputPromise = this.collectRecentOutput(sessionId, 3e3);
645
+ this.terminalManager.write(sessionId, profile.modeCycleKey);
646
+ const recentOutput = await outputPromise;
647
+ const detected = profile.detectMode(recentOutput);
648
+ if (detected) {
649
+ session.currentMode = detected;
650
+ if (detected === targetMode) {
651
+ logger3.info("PTY mode switched", {
652
+ sessionId,
653
+ agentMode,
654
+ targetMode,
655
+ attempts: i + 1
656
+ });
657
+ return;
658
+ }
659
+ }
660
+ }
661
+ logger3.warn("Failed to switch PTY mode after max attempts", {
662
+ sessionId,
663
+ agentMode,
664
+ targetMode,
665
+ current: session.currentMode
666
+ });
667
+ }
668
+ /**
669
+ * Briefly subscribe to PTY output and collect all data emitted during a
670
+ * window. Used by ensurePlanMode to read the mode indicator after pressing
671
+ * the cycle key.
672
+ */
673
+ collectRecentOutput(sessionId, durationMs) {
674
+ return new Promise((resolve) => {
675
+ const chunks = [];
676
+ const subscription = this.terminalManager.onData(sessionId, (data) => {
677
+ chunks.push(stripAnsi(data));
678
+ });
679
+ setTimeout(() => {
680
+ subscription.dispose();
681
+ resolve(chunks.join(""));
682
+ }, durationMs);
683
+ });
684
+ }
685
+ // ---- Native plan mode (two-phase execution) --------------------------------
686
+ /**
687
+ * Whether the agent supports the two-phase native plan strategy:
688
+ * bypass start → Shift+Tab to plan → execute → Shift+Tab to bypass → commit file.
689
+ *
690
+ * Conditions: runner does NOT natively handle plan mode on its own (nativePlanMode=false)
691
+ * AND the PTY profile supports interactive mode switching (modeCycleKey + planModeName).
692
+ */
693
+ shouldUseNativePlan(agentMode) {
694
+ const caps = getRunnerCapabilities(agentMode);
695
+ const profile = getPtyProfile(agentMode);
696
+ return !caps?.nativePlanMode && !!profile?.modeCycleKey && !!profile?.planModeName;
697
+ }
698
+ /**
699
+ * Two-phase plan execution:
700
+ * Phase 1 — Switch to plan mode, send plan prompt, wait for idle (no artifact gate).
701
+ * Phase 2 — Deterministic copy: resolve plan file from CLI-native storage
702
+ * (~/.claude/plans/) and copy it to the expected artifact path.
703
+ *
704
+ * Phase 2 was previously prompt-driven ("ask agent to write file"), which was
705
+ * unreliable because /clear wiped the agent's context. Now it uses PlanFileResolver
706
+ * to find the plan file by snapshot diffing + content validation.
707
+ */
708
+ async runNativePlanMode(sessionId, isNew, options, agentMode, workDir) {
709
+ const continueSession = options.continueSession && !isNew;
710
+ const resolver = PlanFileResolver.forRunner(agentMode);
711
+ resolver.takeBeforeSnapshot();
712
+ const issueIid = extractIidFromPath(workDir);
713
+ const contentHint = issueIid ? PlanFileResolver.buildContentHint(issueIid) : void 0;
714
+ if (continueSession) {
715
+ logger3.info("Native plan mode: continue-session (no /clear, no prompt)", { sessionId });
716
+ } else {
717
+ logger3.info("Native plan mode: switching to plan", { sessionId, agentMode });
718
+ await this.ensurePlanMode(sessionId, agentMode, true, workDir);
719
+ if (!isNew) {
720
+ await this.writeCommand(sessionId, "/clear", agentMode);
721
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
722
+ }
723
+ const promptFile = this.writePromptFile(workDir, options.prompt);
724
+ await this.writeCommand(
725
+ sessionId,
726
+ `Please read and follow all instructions in ${promptFile}`,
727
+ agentMode
728
+ );
729
+ }
730
+ const planArtifactCheck = () => resolver.hasNewOrModifiedFiles();
731
+ const planResult = await this.detectCompletion(sessionId, {
732
+ ...options,
733
+ completionSignal: PLAN_CONFIRM_RE,
734
+ artifactCheck: planArtifactCheck
735
+ }, options.onStreamEvent, continueSession);
736
+ if (planResult.timedOut) {
737
+ logger3.warn("Native plan mode: plan phase timed out", {
738
+ sessionId,
739
+ wasActive: planResult.wasActiveAtTimeout
740
+ });
741
+ return this.buildRunResult(planResult, sessionId);
742
+ }
743
+ logger3.info("Native plan mode: resolving plan file from CLI storage", { sessionId });
744
+ const resolved = resolver.resolve(contentHint);
745
+ if (!resolved) {
746
+ logger3.error("Native plan mode: no plan file found in CLI storage", { sessionId, workDir });
747
+ return {
748
+ success: false,
749
+ output: planResult.output,
750
+ errorMessage: "Plan \u9636\u6BB5\u5B8C\u6210\u4F46\u672A\u5728 CLI \u8BA1\u5212\u76EE\u5F55\u4E2D\u627E\u5230\u8BA1\u5212\u6587\u4EF6",
751
+ sessionId,
752
+ exitCode: null
753
+ };
754
+ }
755
+ const artifactPaths = options.artifactPaths ?? [];
756
+ if (artifactPaths.length > 0) {
757
+ for (const targetPath of artifactPaths) {
758
+ const targetDir = path2.dirname(targetPath);
759
+ if (!fs2.existsSync(targetDir)) {
760
+ fs2.mkdirSync(targetDir, { recursive: true });
761
+ }
762
+ fs2.writeFileSync(targetPath, resolved.content, "utf-8");
763
+ logger3.info("Plan file copied to artifact path", {
764
+ source: path2.basename(resolved.sourcePath),
765
+ target: targetPath,
766
+ size: resolved.content.length
767
+ });
768
+ }
769
+ } else {
770
+ logger3.warn("Native plan mode: no artifactPaths specified, plan file not copied", {
771
+ sessionId,
772
+ resolvedFile: resolved.sourcePath
773
+ });
774
+ }
775
+ logger3.info("Native plan mode completed (deterministic copy)", {
776
+ sessionId,
777
+ planSource: path2.basename(resolved.sourcePath),
778
+ artifactsCopied: artifactPaths.length
779
+ });
780
+ return this.buildRunResult(planResult, sessionId);
781
+ }
782
+ /** Map detectCompletion result to RunResult. */
783
+ buildRunResult(detectResult, sessionId) {
784
+ return {
785
+ success: !detectResult.timedOut,
786
+ output: detectResult.output,
787
+ errorMessage: detectResult.timedOut ? detectResult.timeoutType === "idle" ? "AI \u957F\u65F6\u95F4\u65E0\u54CD\u5E94\uFF0C\u5DF2\u8D85\u65F6\u7EC8\u6B62" : "\u6267\u884C\u8D85\u65F6" : void 0,
788
+ sessionId,
789
+ exitCode: detectResult.timedOut ? null : 0,
790
+ timeoutType: detectResult.timeoutType,
791
+ wasActiveAtTimeout: detectResult.wasActiveAtTimeout
792
+ };
793
+ }
794
+ writePromptFile(workDir, prompt) {
795
+ const dir = path2.join(workDir, ".claude-plan");
796
+ if (!fs2.existsSync(dir)) {
797
+ fs2.mkdirSync(dir, { recursive: true });
798
+ }
799
+ const relPath = ".claude-plan/.phase-prompt.md";
800
+ const absPath = path2.join(workDir, relPath);
801
+ fs2.writeFileSync(absPath, prompt, "utf-8");
802
+ return relPath;
803
+ }
804
+ // ---- Completion detection -------------------------------------------------
805
+ detectCompletion(sessionId, options, onStreamEvent, skipEchoDetection) {
806
+ return new Promise((resolve) => {
807
+ const outputLines = [];
808
+ let lastOutputTime = Date.now();
809
+ let hasSubstantiveOutput = false;
810
+ let echoConsumed = skipEchoDetection ?? false;
811
+ let resolved = false;
812
+ let debounceTimer;
813
+ const DIALOG_BUFFER_MAX = 40;
814
+ const dialogBuffer = [];
815
+ let dialogHandled = false;
816
+ const DIALOG_QUIESCE_MS = 2500;
817
+ let dialogQuiesceTimer;
818
+ let pendingDialogParsed = null;
819
+ const idleTimeoutMs = options.idleTimeoutMs ?? 6e5;
820
+ const timeoutMs = options.timeoutMs;
821
+ const GRACE_WINDOW_MS = options.timeoutGraceMs ?? 6e4;
822
+ const EXTENSION_MS = options.timeoutExtensionMs ?? 6e5;
823
+ const MAX_EXTENSIONS = options.timeoutMaxExtensions ?? 3;
824
+ let extensions = 0;
825
+ const finish = (result) => {
826
+ if (resolved) return;
827
+ resolved = true;
828
+ cleanup();
829
+ resolve(result);
830
+ };
831
+ const scheduleWallTimer = (delayMs) => {
832
+ return setTimeout(() => {
833
+ const recentMs = Date.now() - lastOutputTime;
834
+ const isActive = hasSubstantiveOutput && recentMs < GRACE_WINDOW_MS;
835
+ if (isActive && extensions < MAX_EXTENSIONS) {
836
+ extensions++;
837
+ logger3.info("Wall-clock timeout extended (agent still active)", {
838
+ sessionId,
839
+ extensions,
840
+ maxExtensions: MAX_EXTENSIONS,
841
+ lastOutputAgoMs: recentMs
842
+ });
843
+ wallTimer = scheduleWallTimer(EXTENSION_MS);
844
+ return;
845
+ }
846
+ finish({
847
+ output: outputLines.join(""),
848
+ timedOut: true,
849
+ timeoutType: "wall-clock",
850
+ wasActiveAtTimeout: isActive
851
+ });
852
+ }, delayMs);
853
+ };
854
+ let wallTimer = scheduleWallTimer(timeoutMs);
855
+ const idleCheck = setInterval(() => {
856
+ if (!hasSubstantiveOutput) return;
857
+ if (Date.now() - lastOutputTime >= idleTimeoutMs) {
858
+ finish({
859
+ output: outputLines.join(""),
860
+ timedOut: true,
861
+ timeoutType: "idle",
862
+ wasActiveAtTimeout: false
863
+ });
864
+ }
865
+ }, 5e3);
866
+ const subscription = this.terminalManager.onData(sessionId, (data) => {
867
+ if (resolved) return;
868
+ const stripped = stripAnsi(data);
869
+ const nonEmptyLines = stripped.split("\n").filter((l) => l.trim());
870
+ const isNoise = nonEmptyLines.length === 0 || nonEmptyLines.every((l) => isTuiNoise(l));
871
+ const isIdle = isIdlePrompt(stripped);
872
+ const isMixedFrame = isIdle && containsActiveWork(stripped);
873
+ const hasRealContent = !isNoise && stripped.trim().length > 0;
874
+ if (hasRealContent && (!isIdle || isMixedFrame)) {
875
+ lastOutputTime = Date.now();
876
+ if (dialogQuiesceTimer) {
877
+ clearTimeout(dialogQuiesceTimer);
878
+ dialogQuiesceTimer = void 0;
879
+ pendingDialogParsed = null;
880
+ logger3.info("Dialog quiesce cancelled \u2014 new substantive output arrived", { sessionId });
881
+ }
882
+ }
883
+ if (!echoConsumed && stripped.includes(".phase-prompt.md")) {
884
+ echoConsumed = true;
885
+ return;
886
+ }
887
+ if (QUEUED_MSG_RE.test(stripped)) {
888
+ return;
889
+ }
890
+ if (options.completionSignal && hasSubstantiveOutput && options.completionSignal.test(stripped)) {
891
+ logger3.info("Completion signal detected", { sessionId });
892
+ finish({ output: outputLines.join(""), timedOut: false });
893
+ return;
894
+ }
895
+ if (hasSubstantiveOutput && WORKED_SUMMARY_RE.test(stripped)) {
896
+ const artifactReady = options.artifactCheck ? options.artifactCheck() : true;
897
+ if (artifactReady) {
898
+ logger3.info("Session-end summary detected, finishing", { sessionId });
899
+ finish({ output: outputLines.join(""), timedOut: false });
900
+ return;
901
+ }
902
+ logger3.info("Session summary detected but artifacts not ready, continuing", { sessionId });
903
+ }
904
+ if (PERMISSION_DIALOG_RE.test(stripped)) {
905
+ logger3.info("Permission dialog detected, auto-confirming", { sessionId });
906
+ setTimeout(() => {
907
+ if (!resolved) this.terminalManager.write(sessionId, "\r");
908
+ }, 500);
909
+ return;
910
+ }
911
+ if (options.onInputRequired && !dialogHandled && isInteractiveDialog(stripped)) {
912
+ const parsed = parseInteractiveDialog(stripped);
913
+ if (parsed) {
914
+ const confidence = getDialogConfidence(stripped);
915
+ if (confidence === "high") {
916
+ dialogHandled = true;
917
+ dialogBuffer.length = 0;
918
+ if (dialogQuiesceTimer) {
919
+ clearTimeout(dialogQuiesceTimer);
920
+ dialogQuiesceTimer = void 0;
921
+ }
922
+ logger3.info("Interactive dialog detected (high confidence), forwarding to handler", {
923
+ sessionId,
924
+ question: parsed.question.slice(0, 80),
925
+ optionCount: parsed.options.length
926
+ });
927
+ options.onInputRequired({
928
+ type: "interactive-dialog",
929
+ content: parsed.question,
930
+ options: parsed.options
931
+ }).then((response) => {
932
+ dialogHandled = false;
933
+ if (!resolved && response) {
934
+ this.terminalManager.write(sessionId, response + "\r");
935
+ }
936
+ }).catch((err) => {
937
+ logger3.warn("onInputRequired callback failed for interactive dialog", {
938
+ error: err.message
939
+ });
940
+ dialogHandled = false;
941
+ });
942
+ return;
943
+ }
944
+ if (!dialogQuiesceTimer) {
945
+ pendingDialogParsed = parsed;
946
+ logger3.info("Interactive dialog detected (low confidence), starting quiesce", {
947
+ sessionId,
948
+ question: parsed.question.slice(0, 80),
949
+ optionCount: parsed.options.length
950
+ });
951
+ dialogQuiesceTimer = setTimeout(() => {
952
+ dialogQuiesceTimer = void 0;
953
+ if (resolved || dialogHandled || !pendingDialogParsed) return;
954
+ dialogHandled = true;
955
+ dialogBuffer.length = 0;
956
+ const dp = pendingDialogParsed;
957
+ pendingDialogParsed = null;
958
+ logger3.info("Dialog quiesce elapsed \u2014 forwarding low-confidence dialog", {
959
+ sessionId,
960
+ question: dp.question.slice(0, 80)
961
+ });
962
+ options.onInputRequired({
963
+ type: "interactive-dialog",
964
+ content: dp.question,
965
+ options: dp.options
966
+ }).then((response) => {
967
+ dialogHandled = false;
968
+ if (!resolved && response) {
969
+ this.terminalManager.write(sessionId, response + "\r");
970
+ }
971
+ }).catch((err) => {
972
+ logger3.warn("onInputRequired callback failed (quiesced dialog)", {
973
+ error: err.message
974
+ });
975
+ dialogHandled = false;
976
+ });
977
+ }, DIALOG_QUIESCE_MS);
978
+ }
979
+ return;
980
+ }
981
+ }
982
+ if (echoConsumed && !hasSubstantiveOutput && !isNoise && stripped.trim().length > 0) {
983
+ hasSubstantiveOutput = true;
984
+ }
985
+ outputLines.push(stripped);
986
+ if (onStreamEvent) {
987
+ for (const line of stripped.split("\n").filter((l) => l.trim())) {
988
+ if (!isTuiNoise(line)) {
989
+ onStreamEvent({
990
+ type: "raw",
991
+ content: line,
992
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
993
+ });
994
+ }
995
+ }
996
+ }
997
+ if (!dialogHandled && !isNoise) {
998
+ for (const line of stripped.split("\n").filter((l) => l.trim())) {
999
+ if (!isTuiNoise(line)) {
1000
+ dialogBuffer.push(line);
1001
+ if (dialogBuffer.length > DIALOG_BUFFER_MAX) dialogBuffer.shift();
1002
+ }
1003
+ }
1004
+ }
1005
+ if (hasSubstantiveOutput && isIdlePrompt(stripped)) {
1006
+ if (isMixedFrame) {
1007
+ } else if (options.completionSignal) {
1008
+ logger3.debug("Idle prompt ignored (waiting for completionSignal)", { sessionId });
1009
+ } else if (debounceTimer && isNoise) {
1010
+ } else {
1011
+ if (options.onInputRequired && !dialogHandled && dialogBuffer.length >= 2) {
1012
+ const combined = dialogBuffer.join("\n");
1013
+ if (isInteractiveDialog(combined)) {
1014
+ const parsed = parseInteractiveDialog(combined);
1015
+ if (parsed) {
1016
+ const bufConfidence = getDialogConfidence(combined);
1017
+ if (bufConfidence === "high") {
1018
+ dialogHandled = true;
1019
+ dialogBuffer.length = 0;
1020
+ if (dialogQuiesceTimer) {
1021
+ clearTimeout(dialogQuiesceTimer);
1022
+ dialogQuiesceTimer = void 0;
1023
+ }
1024
+ logger3.info("Interactive dialog detected via accumulated buffer (high confidence)", {
1025
+ sessionId,
1026
+ question: parsed.question.slice(0, 80),
1027
+ optionCount: parsed.options.length
1028
+ });
1029
+ options.onInputRequired({
1030
+ type: "interactive-dialog",
1031
+ content: parsed.question,
1032
+ options: parsed.options
1033
+ }).then((response) => {
1034
+ dialogHandled = false;
1035
+ if (!resolved && response) {
1036
+ this.terminalManager.write(sessionId, response + "\r");
1037
+ }
1038
+ }).catch((err) => {
1039
+ logger3.warn("onInputRequired callback failed (accumulated)", {
1040
+ error: err.message
1041
+ });
1042
+ dialogHandled = false;
1043
+ });
1044
+ return;
1045
+ }
1046
+ if (!dialogQuiesceTimer) {
1047
+ pendingDialogParsed = parsed;
1048
+ logger3.info("Dialog detected via buffer (low confidence), starting quiesce", {
1049
+ sessionId,
1050
+ question: parsed.question.slice(0, 80)
1051
+ });
1052
+ dialogQuiesceTimer = setTimeout(() => {
1053
+ dialogQuiesceTimer = void 0;
1054
+ if (resolved || dialogHandled || !pendingDialogParsed) return;
1055
+ dialogHandled = true;
1056
+ dialogBuffer.length = 0;
1057
+ const dp = pendingDialogParsed;
1058
+ pendingDialogParsed = null;
1059
+ logger3.info("Buffer dialog quiesce elapsed \u2014 forwarding", { sessionId });
1060
+ options.onInputRequired({
1061
+ type: "interactive-dialog",
1062
+ content: dp.question,
1063
+ options: dp.options
1064
+ }).then((response) => {
1065
+ dialogHandled = false;
1066
+ if (!resolved && response) {
1067
+ this.terminalManager.write(sessionId, response + "\r");
1068
+ }
1069
+ }).catch((err) => {
1070
+ logger3.warn("onInputRequired callback failed (buffer quiesced)", {
1071
+ error: err.message
1072
+ });
1073
+ dialogHandled = false;
1074
+ });
1075
+ }, DIALOG_QUIESCE_MS);
1076
+ }
1077
+ }
1078
+ }
1079
+ }
1080
+ if (debounceTimer) clearTimeout(debounceTimer);
1081
+ const scheduleDebounce = () => {
1082
+ debounceTimer = setTimeout(() => {
1083
+ if (resolved) return;
1084
+ const recentActivityMs = Date.now() - lastOutputTime;
1085
+ if (recentActivityMs < 5e3) {
1086
+ scheduleDebounce();
1087
+ return;
1088
+ }
1089
+ const artifactReady = options.artifactCheck ? options.artifactCheck() : true;
1090
+ if (!artifactReady) {
1091
+ logger3.info("Idle prompt detected but artifacts not ready, continuing to wait", {
1092
+ sessionId
1093
+ });
1094
+ scheduleDebounce();
1095
+ return;
1096
+ }
1097
+ finish({
1098
+ output: outputLines.join(""),
1099
+ timedOut: false,
1100
+ timeoutType: void 0
1101
+ });
1102
+ }, 5e3);
1103
+ };
1104
+ scheduleDebounce();
1105
+ }
1106
+ }
1107
+ });
1108
+ this.terminalManager.onExit(sessionId, (exitCode) => {
1109
+ const wasKilled = this.killedSessions.delete(sessionId);
1110
+ for (const [wd, info] of this.sessions) {
1111
+ if (info.sessionId === sessionId) {
1112
+ this.sessions.delete(wd);
1113
+ break;
1114
+ }
1115
+ }
1116
+ if (!resolved) {
1117
+ logger3.warn("PTY process exited during phase", { sessionId, exitCode, wasKilled });
1118
+ finish({
1119
+ output: outputLines.join(""),
1120
+ timedOut: wasKilled,
1121
+ timeoutType: wasKilled ? "wall-clock" : void 0
1122
+ });
1123
+ } else {
1124
+ logger3.info("PTY exited after phase completion (post stop-hook)", { sessionId, exitCode });
1125
+ }
1126
+ });
1127
+ const cleanup = () => {
1128
+ clearTimeout(wallTimer);
1129
+ clearInterval(idleCheck);
1130
+ if (debounceTimer) clearTimeout(debounceTimer);
1131
+ if (dialogQuiesceTimer) clearTimeout(dialogQuiesceTimer);
1132
+ subscription.dispose();
1133
+ };
1134
+ });
1135
+ }
1136
+ };
1137
+
1138
+ export {
1139
+ PlanFileResolver,
1140
+ stripAnsi,
1141
+ isIdlePrompt,
1142
+ TRUST_DIALOG_RE,
1143
+ PERMISSION_DIALOG_RE,
1144
+ PLAN_CONFIRM_RE,
1145
+ INTERACTIVE_NAV_HINT_RE,
1146
+ parseInteractiveDialog,
1147
+ getDialogConfidence,
1148
+ isInteractiveDialog,
1149
+ containsActiveWork,
1150
+ isTuiNoise,
1151
+ PtyRunner
1152
+ };
1153
+ //# sourceMappingURL=chunk-4XMYOXGZ.js.map