@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.
- package/dist/{AIRunnerRegistry-II3WWSFN.js → AIRunnerRegistry-CFDNWSXC.js} +6 -3
- package/dist/{LockNote-Z2CLDZNN.js → LockNote-W2JNVMW7.js} +3 -3
- package/dist/PtyRunner-NYASBTRP.js +33 -0
- package/dist/SdkRunner-U2OTOMZU.js +9 -0
- package/dist/ai-runner/AIRunner.d.ts +19 -1
- package/dist/ai-runner/AIRunner.d.ts.map +1 -1
- package/dist/ai-runner/AIRunnerRegistry.d.ts +8 -0
- package/dist/ai-runner/AIRunnerRegistry.d.ts.map +1 -1
- package/dist/ai-runner/PlanFileResolver.d.ts +53 -0
- package/dist/ai-runner/PlanFileResolver.d.ts.map +1 -0
- package/dist/ai-runner/PtyRunner.d.ts +45 -4
- package/dist/ai-runner/PtyRunner.d.ts.map +1 -1
- package/dist/ai-runner/SdkRunner.d.ts +22 -0
- package/dist/ai-runner/SdkRunner.d.ts.map +1 -0
- package/dist/ai-runner/index.d.ts +5 -2
- package/dist/ai-runner/index.d.ts.map +1 -1
- package/dist/ai-runner/sdk/ClaudeCodeSDK.d.ts +37 -0
- package/dist/ai-runner/sdk/ClaudeCodeSDK.d.ts.map +1 -0
- package/dist/ai-runner/sdk/Stream.d.ts +22 -0
- package/dist/ai-runner/sdk/Stream.d.ts.map +1 -0
- package/dist/ai-runner/sdk/types.d.ts +146 -0
- package/dist/ai-runner/sdk/types.d.ts.map +1 -0
- package/dist/{ai-runner-HLA44WI6.js → ai-runner-TOHVJJ76.js} +14 -5
- package/dist/{analyze-ZIXNC5GN.js → analyze-DBH4K3J7.js} +8 -6
- package/dist/{analyze-ZIXNC5GN.js.map → analyze-DBH4K3J7.js.map} +1 -1
- package/dist/{braindump-56WAY2RD.js → braindump-RYI4BGMG.js} +11 -9
- package/dist/{braindump-56WAY2RD.js.map → braindump-RYI4BGMG.js.map} +1 -1
- package/dist/{chunk-AVGZH64A.js → chunk-2RWGZPNF.js} +4 -1
- package/dist/chunk-2RWGZPNF.js.map +1 -0
- package/dist/chunk-4XMYOXGZ.js +1153 -0
- package/dist/chunk-4XMYOXGZ.js.map +1 -0
- package/dist/{chunk-UBQLXQ7I.js → chunk-5JBADEKR.js} +7 -7
- package/dist/{chunk-M5C2WILQ.js → chunk-5M5SB6ZA.js} +7 -5
- package/dist/{chunk-M5C2WILQ.js.map → chunk-5M5SB6ZA.js.map} +1 -1
- package/dist/{chunk-HDFNMVRQ.js → chunk-DVNAH2GV.js} +2 -2
- package/dist/{chunk-GXFG4JU6.js → chunk-EU4XFZ2T.js} +2 -2
- package/dist/{chunk-NZHKAPU6.js → chunk-FJTZKAJA.js} +9 -3
- package/dist/chunk-FJTZKAJA.js.map +1 -0
- package/dist/chunk-G7QI5WDI.js +14 -0
- package/dist/chunk-G7QI5WDI.js.map +1 -0
- package/dist/{chunk-2YQHKXLL.js → chunk-GPZX4DSY.js} +22 -6
- package/dist/chunk-GPZX4DSY.js.map +1 -0
- package/dist/{chunk-IP3QTP5A.js → chunk-IWSMQXBL.js} +189 -48
- package/dist/chunk-IWSMQXBL.js.map +1 -0
- package/dist/{chunk-O3WEV5W3.js → chunk-JMACM7AJ.js} +47 -9
- package/dist/chunk-JMACM7AJ.js.map +1 -0
- package/dist/chunk-MSL7ROVK.js +1 -0
- package/dist/{chunk-YCYVNRLF.js → chunk-OBGEEGQ3.js} +61 -19
- package/dist/chunk-OBGEEGQ3.js.map +1 -0
- package/dist/chunk-R32Q3RGK.js +666 -0
- package/dist/chunk-R32Q3RGK.js.map +1 -0
- package/dist/{chunk-SAMTXC4A.js → chunk-TFEPHOVE.js} +12 -17
- package/dist/chunk-TFEPHOVE.js.map +1 -0
- package/dist/{chunk-QZZGIZWC.js → chunk-XSX3PGQW.js} +63 -20
- package/dist/chunk-XSX3PGQW.js.map +1 -0
- package/dist/{chunk-2MESXJEZ.js → chunk-YNRKPQLS.js} +3 -3
- package/dist/cli/setup/PreflightChecker.d.ts +1 -0
- package/dist/cli/setup/PreflightChecker.d.ts.map +1 -1
- package/dist/cli/setup/env-metadata.d.ts.map +1 -1
- package/dist/cli.js +10 -9
- package/dist/cli.js.map +1 -1
- package/dist/{config-WTRSZLOC.js → config-23TBYFP5.js} +5 -4
- package/dist/config-schema.d.ts +6 -0
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/{doctor-37JNBGDN.js → doctor-ZG3DO7J5.js} +3 -3
- package/dist/errors/AIExecutionError.d.ts +3 -0
- package/dist/errors/AIExecutionError.d.ts.map +1 -1
- package/dist/{errors-S3BWYA4I.js → errors-J3ZRP66W.js} +2 -2
- package/dist/events/EventBus.d.ts +1 -1
- package/dist/events/EventBus.d.ts.map +1 -1
- package/dist/i18n/locales/en.d.ts.map +1 -1
- package/dist/i18n/locales/zh-CN.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -14
- package/dist/{init-QQDXGTPB.js → init-37DLQ5AJ.js} +9 -8
- package/dist/{init-QQDXGTPB.js.map → init-37DLQ5AJ.js.map} +1 -1
- package/dist/lib.js +10 -8
- package/dist/lib.js.map +1 -1
- package/dist/orchestrator/PendingDialogStore.d.ts +12 -0
- package/dist/orchestrator/PendingDialogStore.d.ts.map +1 -0
- package/dist/orchestrator/steps/FailureHandler.d.ts.map +1 -1
- package/dist/orchestrator/steps/PhaseLoopStep.d.ts.map +1 -1
- package/dist/persistence/PlanPersistence.d.ts +5 -0
- package/dist/persistence/PlanPersistence.d.ts.map +1 -1
- package/dist/persistence/TodolistExtractor.d.ts +31 -0
- package/dist/persistence/TodolistExtractor.d.ts.map +1 -0
- package/dist/phases/BasePhase.d.ts.map +1 -1
- package/dist/phases/PhaseOutcome.d.ts +2 -0
- package/dist/phases/PhaseOutcome.d.ts.map +1 -1
- package/dist/phases/PlanPhase.d.ts.map +1 -1
- package/dist/prompts/templates.d.ts +2 -2
- package/dist/prompts/templates.d.ts.map +1 -1
- package/dist/{restart-BMILTP5X.js → restart-C7QBXT44.js} +9 -8
- package/dist/{restart-BMILTP5X.js.map → restart-C7QBXT44.js.map} +1 -1
- package/dist/run.js +16 -14
- package/dist/run.js.map +1 -1
- package/dist/start-66JO56AW.js +16 -0
- package/dist/start-66JO56AW.js.map +1 -0
- package/dist/tracker/IssueTracker.d.ts +6 -0
- package/dist/tracker/IssueTracker.d.ts.map +1 -1
- package/dist/web/routes/api.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/web/frontend/dist/assets/index-DJzC2saL.css +1 -0
- package/src/web/frontend/dist/assets/{index-D_oTMuJU.js → index-Mnu8M3ww.js} +57 -57
- package/src/web/frontend/dist/index.html +2 -2
- package/dist/PtyRunner-6UGI5STW.js +0 -22
- package/dist/chunk-2YQHKXLL.js.map +0 -1
- package/dist/chunk-AVGZH64A.js.map +0 -1
- package/dist/chunk-IP3QTP5A.js.map +0 -1
- package/dist/chunk-NZHKAPU6.js.map +0 -1
- package/dist/chunk-O3WEV5W3.js.map +0 -1
- package/dist/chunk-QZZGIZWC.js.map +0 -1
- package/dist/chunk-SAMTXC4A.js.map +0 -1
- package/dist/chunk-U237JSLB.js +0 -1
- package/dist/chunk-U6GWFTKA.js +0 -657
- package/dist/chunk-U6GWFTKA.js.map +0 -1
- package/dist/chunk-YCYVNRLF.js.map +0 -1
- package/dist/start-6QRW6IJI.js +0 -15
- package/src/web/frontend/dist/assets/index-COYziOhv.css +0 -1
- /package/dist/{AIRunnerRegistry-II3WWSFN.js.map → AIRunnerRegistry-CFDNWSXC.js.map} +0 -0
- /package/dist/{LockNote-Z2CLDZNN.js.map → LockNote-W2JNVMW7.js.map} +0 -0
- /package/dist/{PtyRunner-6UGI5STW.js.map → PtyRunner-NYASBTRP.js.map} +0 -0
- /package/dist/{ai-runner-HLA44WI6.js.map → SdkRunner-U2OTOMZU.js.map} +0 -0
- /package/dist/{chunk-U237JSLB.js.map → ai-runner-TOHVJJ76.js.map} +0 -0
- /package/dist/{chunk-UBQLXQ7I.js.map → chunk-5JBADEKR.js.map} +0 -0
- /package/dist/{chunk-HDFNMVRQ.js.map → chunk-DVNAH2GV.js.map} +0 -0
- /package/dist/{chunk-GXFG4JU6.js.map → chunk-EU4XFZ2T.js.map} +0 -0
- /package/dist/{config-WTRSZLOC.js.map → chunk-MSL7ROVK.js.map} +0 -0
- /package/dist/{chunk-2MESXJEZ.js.map → chunk-YNRKPQLS.js.map} +0 -0
- /package/dist/{errors-S3BWYA4I.js.map → config-23TBYFP5.js.map} +0 -0
- /package/dist/{doctor-37JNBGDN.js.map → doctor-ZG3DO7J5.js.map} +0 -0
- /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
|