@xdevops/issue-auto-finish 1.0.92 → 1.0.93
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/{PtyRunner-NYASBTRP.js → PtyRunner-XMWDMH3L.js} +4 -2
- package/dist/ai-runner/DialogClassifier.d.ts +44 -0
- package/dist/ai-runner/DialogClassifier.d.ts.map +1 -0
- package/dist/ai-runner/PtyRunner.d.ts +17 -0
- package/dist/ai-runner/PtyRunner.d.ts.map +1 -1
- package/dist/{ai-runner-TOHVJJ76.js → ai-runner-S2ATTGWX.js} +2 -2
- package/dist/{analyze-DBH4K3J7.js → analyze-DAVYPBHK.js} +2 -2
- package/dist/{braindump-RYI4BGMG.js → braindump-A4R3A4QT.js} +2 -2
- package/dist/{chunk-ENF24C44.js → chunk-2XACBKPB.js} +2 -2
- package/dist/{chunk-6T7ZHAV2.js → chunk-BPVRMZU4.js} +9 -9
- package/dist/{chunk-4XMYOXGZ.js → chunk-HD6V7KPE.js} +878 -67
- package/dist/chunk-HD6V7KPE.js.map +1 -0
- package/dist/{chunk-WZGEYHCC.js → chunk-OPWP73PW.js} +271 -716
- package/dist/chunk-OPWP73PW.js.map +1 -0
- package/dist/{chunk-2WDVTLVF.js → chunk-SNSEW7DS.js} +1 -1
- package/dist/cli.js +5 -5
- package/dist/hooks/HookInjector.d.ts +14 -0
- package/dist/hooks/HookInjector.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/{init-UKTP7LXS.js → init-OD7CLRWK.js} +2 -2
- package/dist/lib.js +2 -2
- package/dist/{restart-5D3ZDD5L.js → restart-JVVOYC6C.js} +2 -2
- package/dist/run.js +4 -4
- package/dist/{start-IQBNXLEI.js → start-INU24RRG.js} +2 -2
- package/package.json +1 -1
- package/src/web/frontend/dist/assets/index-BrvoaFSK.css +1 -0
- package/src/web/frontend/dist/assets/{index-BR0UoQER.js → index-CmyxgdS_.js} +54 -54
- package/src/web/frontend/dist/index.html +2 -2
- package/dist/chunk-4XMYOXGZ.js.map +0 -1
- package/dist/chunk-WZGEYHCC.js.map +0 -1
- package/src/web/frontend/dist/assets/index-DWOHf3bd.css +0 -1
- /package/dist/{PtyRunner-NYASBTRP.js.map → PtyRunner-XMWDMH3L.js.map} +0 -0
- /package/dist/{ai-runner-TOHVJJ76.js.map → ai-runner-S2ATTGWX.js.map} +0 -0
- /package/dist/{analyze-DBH4K3J7.js.map → analyze-DAVYPBHK.js.map} +0 -0
- /package/dist/{braindump-RYI4BGMG.js.map → braindump-A4R3A4QT.js.map} +0 -0
- /package/dist/{chunk-ENF24C44.js.map → chunk-2XACBKPB.js.map} +0 -0
- /package/dist/{chunk-6T7ZHAV2.js.map → chunk-BPVRMZU4.js.map} +0 -0
- /package/dist/{chunk-2WDVTLVF.js.map → chunk-SNSEW7DS.js.map} +0 -0
- /package/dist/{init-UKTP7LXS.js.map → init-OD7CLRWK.js.map} +0 -0
- /package/dist/{restart-5D3ZDD5L.js.map → restart-JVVOYC6C.js.map} +0 -0
- /package/dist/{start-IQBNXLEI.js.map → start-INU24RRG.js.map} +0 -0
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
} from "./chunk-GF2RRYHB.js";
|
|
13
13
|
|
|
14
14
|
// src/ai-runner/PtyRunner.ts
|
|
15
|
-
import
|
|
16
|
-
import
|
|
15
|
+
import fs4 from "fs";
|
|
16
|
+
import path3 from "path";
|
|
17
17
|
|
|
18
18
|
// src/ai-runner/PlanFileResolver.ts
|
|
19
19
|
import fs from "fs";
|
|
@@ -200,8 +200,699 @@ var PlanFileResolver = class _PlanFileResolver {
|
|
|
200
200
|
}
|
|
201
201
|
};
|
|
202
202
|
|
|
203
|
+
// src/ai-runner/DialogClassifier.ts
|
|
204
|
+
var logger3 = logger.child("DialogClassifier");
|
|
205
|
+
var HOOK_EVENT_TTL_MS = 5e3;
|
|
206
|
+
var PERMISSION_SUPPRESS_MS = 2e3;
|
|
207
|
+
var ASK_USER_SUPPRESS_MS = 3e3;
|
|
208
|
+
var DialogClassifier = class {
|
|
209
|
+
recentEvents = [];
|
|
210
|
+
handledIds = /* @__PURE__ */ new Set();
|
|
211
|
+
ingestHookEvent(event) {
|
|
212
|
+
this.recentEvents.push({ event, receivedAt: Date.now() });
|
|
213
|
+
this.pruneExpired();
|
|
214
|
+
if (event.event === "ask_user_question") {
|
|
215
|
+
logger3.info("Received ask_user_question hook event", {
|
|
216
|
+
question: event.question.slice(0, 80),
|
|
217
|
+
optionCount: event.options.length
|
|
218
|
+
});
|
|
219
|
+
} else if (event.event === "notification") {
|
|
220
|
+
logger3.debug("Received notification hook event", {
|
|
221
|
+
type: event.notification_type
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Check if there's a pending AskUserQuestion from hooks that hasn't been
|
|
227
|
+
* handled yet. Returns structured data or null.
|
|
228
|
+
*/
|
|
229
|
+
consumePendingAskUser() {
|
|
230
|
+
const now = Date.now();
|
|
231
|
+
for (let i = this.recentEvents.length - 1; i >= 0; i--) {
|
|
232
|
+
const { event, receivedAt } = this.recentEvents[i];
|
|
233
|
+
if (event.event !== "ask_user_question") continue;
|
|
234
|
+
if (now - receivedAt > HOOK_EVENT_TTL_MS) continue;
|
|
235
|
+
const eventId = `ask_user_${event.ts}`;
|
|
236
|
+
if (this.handledIds.has(eventId)) continue;
|
|
237
|
+
this.handledIds.add(eventId);
|
|
238
|
+
return {
|
|
239
|
+
question: event.question,
|
|
240
|
+
options: mapHookOptions(event.options)
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Whether a recent permission_prompt notification exists, indicating
|
|
247
|
+
* that regex-detected dialogs are likely false positives.
|
|
248
|
+
*/
|
|
249
|
+
hasRecentPermissionPrompt() {
|
|
250
|
+
const now = Date.now();
|
|
251
|
+
return this.recentEvents.some(
|
|
252
|
+
({ event, receivedAt }) => event.event === "notification" && event.notification_type === "permission_prompt" && now - receivedAt < PERMISSION_SUPPRESS_MS
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Whether a recent ask_user_question hook event exists, indicating
|
|
257
|
+
* that regex detection should be suppressed to avoid duplicate forwarding.
|
|
258
|
+
*/
|
|
259
|
+
hasRecentAskUser() {
|
|
260
|
+
const now = Date.now();
|
|
261
|
+
return this.recentEvents.some(
|
|
262
|
+
({ event, receivedAt }) => event.event === "ask_user_question" && now - receivedAt < ASK_USER_SUPPRESS_MS
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Whether regex-based dialog detection should be suppressed
|
|
267
|
+
* (either due to recent hook-based AskUser or permission_prompt).
|
|
268
|
+
*/
|
|
269
|
+
shouldSuppressRegex() {
|
|
270
|
+
return this.hasRecentAskUser() || this.hasRecentPermissionPrompt();
|
|
271
|
+
}
|
|
272
|
+
reset() {
|
|
273
|
+
this.recentEvents.length = 0;
|
|
274
|
+
this.handledIds.clear();
|
|
275
|
+
}
|
|
276
|
+
pruneExpired() {
|
|
277
|
+
const cutoff = Date.now() - HOOK_EVENT_TTL_MS;
|
|
278
|
+
this.recentEvents = this.recentEvents.filter((e) => e.receivedAt > cutoff);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
function mapHookOptions(hookOptions) {
|
|
282
|
+
return hookOptions.map((o, i) => ({
|
|
283
|
+
index: o.index ?? i + 1,
|
|
284
|
+
label: o.label
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/hooks/HookEventWatcher.ts
|
|
289
|
+
import fs2 from "fs";
|
|
290
|
+
var logger4 = logger.child("HookEventWatcher");
|
|
291
|
+
var HookEventWatcher = class {
|
|
292
|
+
eventsFile;
|
|
293
|
+
watcher = null;
|
|
294
|
+
pollTimer = null;
|
|
295
|
+
offset = 0;
|
|
296
|
+
listeners = [];
|
|
297
|
+
started = false;
|
|
298
|
+
constructor(eventsFile) {
|
|
299
|
+
this.eventsFile = eventsFile;
|
|
300
|
+
}
|
|
301
|
+
start() {
|
|
302
|
+
if (this.started) return;
|
|
303
|
+
this.started = true;
|
|
304
|
+
this.offset = this.getCurrentSize();
|
|
305
|
+
try {
|
|
306
|
+
this.watcher = fs2.watch(this.eventsFile, () => this.readNewEvents());
|
|
307
|
+
} catch {
|
|
308
|
+
logger4.debug("fs.watch unavailable, using poll-only mode");
|
|
309
|
+
}
|
|
310
|
+
this.pollTimer = setInterval(() => this.readNewEvents(), 1e3);
|
|
311
|
+
}
|
|
312
|
+
stop() {
|
|
313
|
+
if (!this.started) return;
|
|
314
|
+
this.started = false;
|
|
315
|
+
this.watcher?.close();
|
|
316
|
+
this.watcher = null;
|
|
317
|
+
if (this.pollTimer) {
|
|
318
|
+
clearInterval(this.pollTimer);
|
|
319
|
+
this.pollTimer = null;
|
|
320
|
+
}
|
|
321
|
+
this.listeners = [];
|
|
322
|
+
}
|
|
323
|
+
onEvent(callback) {
|
|
324
|
+
this.listeners.push(callback);
|
|
325
|
+
return {
|
|
326
|
+
dispose: () => {
|
|
327
|
+
this.listeners = this.listeners.filter((l) => l !== callback);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* 等待指定类型的事件,带超时。
|
|
333
|
+
* 对于 'stop' 事件,只有 blocked=false 时才 resolve。
|
|
334
|
+
*/
|
|
335
|
+
waitForEvent(eventType, timeoutMs) {
|
|
336
|
+
return new Promise((resolve, reject) => {
|
|
337
|
+
const timer = setTimeout(() => {
|
|
338
|
+
sub.dispose();
|
|
339
|
+
reject(new Error(`Timeout waiting for hook event "${eventType}" after ${timeoutMs}ms`));
|
|
340
|
+
}, timeoutMs);
|
|
341
|
+
const sub = this.onEvent((ev) => {
|
|
342
|
+
if (ev.event !== eventType) return;
|
|
343
|
+
if (ev.event === "stop" && ev.blocked) return;
|
|
344
|
+
clearTimeout(timer);
|
|
345
|
+
sub.dispose();
|
|
346
|
+
resolve(ev);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/** 获取已记录的所有产物写入事件摘要 */
|
|
351
|
+
getArtifactSummary() {
|
|
352
|
+
const events = this.readAll().filter(
|
|
353
|
+
(e) => e.event === "artifact_write"
|
|
354
|
+
);
|
|
355
|
+
if (events.length === 0) return "";
|
|
356
|
+
return events.map((e) => `${e.file} (${e.bytes} bytes)`).join(", ");
|
|
357
|
+
}
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
// Private
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
readNewEvents() {
|
|
362
|
+
if (!this.started) return;
|
|
363
|
+
const size = this.getCurrentSize();
|
|
364
|
+
if (size <= this.offset) return;
|
|
365
|
+
try {
|
|
366
|
+
const fd = fs2.openSync(this.eventsFile, "r");
|
|
367
|
+
try {
|
|
368
|
+
const buf = Buffer.alloc(size - this.offset);
|
|
369
|
+
fs2.readSync(fd, buf, 0, buf.length, this.offset);
|
|
370
|
+
this.offset = size;
|
|
371
|
+
const chunk = buf.toString("utf-8");
|
|
372
|
+
for (const line of chunk.split("\n")) {
|
|
373
|
+
const trimmed = line.trim();
|
|
374
|
+
if (!trimmed) continue;
|
|
375
|
+
try {
|
|
376
|
+
const event = JSON.parse(trimmed);
|
|
377
|
+
this.emit(event);
|
|
378
|
+
} catch {
|
|
379
|
+
logger4.debug("Skipping malformed event line", { line: trimmed });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} finally {
|
|
383
|
+
fs2.closeSync(fd);
|
|
384
|
+
}
|
|
385
|
+
} catch (err) {
|
|
386
|
+
logger4.debug("Error reading events file", { error: err.message });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
readAll() {
|
|
390
|
+
if (!fs2.existsSync(this.eventsFile)) return [];
|
|
391
|
+
try {
|
|
392
|
+
const content = fs2.readFileSync(this.eventsFile, "utf-8").trim();
|
|
393
|
+
if (!content) return [];
|
|
394
|
+
return content.split("\n").reduce((acc, line) => {
|
|
395
|
+
const trimmed = line.trim();
|
|
396
|
+
if (!trimmed) return acc;
|
|
397
|
+
try {
|
|
398
|
+
acc.push(JSON.parse(trimmed));
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
return acc;
|
|
402
|
+
}, []);
|
|
403
|
+
} catch {
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
emit(event) {
|
|
408
|
+
for (const listener of this.listeners) {
|
|
409
|
+
try {
|
|
410
|
+
listener(event);
|
|
411
|
+
} catch (err) {
|
|
412
|
+
logger4.warn("Event listener error", { error: err.message });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
getCurrentSize() {
|
|
417
|
+
try {
|
|
418
|
+
return fs2.statSync(this.eventsFile).size;
|
|
419
|
+
} catch {
|
|
420
|
+
return 0;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// src/hooks/HookInjector.ts
|
|
426
|
+
import fs3 from "fs";
|
|
427
|
+
import path2 from "path";
|
|
428
|
+
var logger5 = logger.child("HookInjector");
|
|
429
|
+
var HOOKS_DIR = ".claude-plan/.hooks";
|
|
430
|
+
var EVENTS_FILE_NAME = ".hook-events.jsonl";
|
|
431
|
+
var MANIFEST_FILE_NAME = ".artifact-manifest.jsonl";
|
|
432
|
+
var CONTEXT_FILE_NAME = ".hook-context.json";
|
|
433
|
+
var HookInjector = class {
|
|
434
|
+
inject(ctx) {
|
|
435
|
+
this.writeHookScripts(ctx);
|
|
436
|
+
this.writeContextFile(ctx);
|
|
437
|
+
this.writeSettingsLocal(ctx);
|
|
438
|
+
this.initEventsFile(ctx);
|
|
439
|
+
logger5.info("Hooks injected", {
|
|
440
|
+
workDir: ctx.workDir,
|
|
441
|
+
issueIid: ctx.issueIid,
|
|
442
|
+
phase: ctx.phaseName,
|
|
443
|
+
artifacts: ctx.expectedArtifacts
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* 阶段切换时更新 hooks 配置(重写脚本 + settings.local.json)。
|
|
448
|
+
* 保留 events/manifest 文件(不截断),仅更新脚本和配置。
|
|
449
|
+
*/
|
|
450
|
+
updateForPhase(ctx) {
|
|
451
|
+
this.writeHookScripts(ctx);
|
|
452
|
+
this.writeContextFile(ctx);
|
|
453
|
+
this.writeSettingsLocal(ctx);
|
|
454
|
+
logger5.info("Hooks updated for phase", {
|
|
455
|
+
workDir: ctx.workDir,
|
|
456
|
+
issueIid: ctx.issueIid,
|
|
457
|
+
phase: ctx.phaseName
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
readManifest(workDir) {
|
|
461
|
+
const manifestPath = path2.join(workDir, ".claude-plan", MANIFEST_FILE_NAME);
|
|
462
|
+
return readJsonl(manifestPath);
|
|
463
|
+
}
|
|
464
|
+
readEvents(workDir) {
|
|
465
|
+
const eventsPath = path2.join(workDir, ".claude-plan", EVENTS_FILE_NAME);
|
|
466
|
+
return readJsonl(eventsPath);
|
|
467
|
+
}
|
|
468
|
+
getEventsFilePath(workDir) {
|
|
469
|
+
return path2.join(workDir, ".claude-plan", EVENTS_FILE_NAME);
|
|
470
|
+
}
|
|
471
|
+
getManifestFilePath(workDir) {
|
|
472
|
+
return path2.join(workDir, ".claude-plan", MANIFEST_FILE_NAME);
|
|
473
|
+
}
|
|
474
|
+
cleanup(workDir) {
|
|
475
|
+
const hooksDir = path2.join(workDir, HOOKS_DIR);
|
|
476
|
+
try {
|
|
477
|
+
if (fs3.existsSync(hooksDir)) {
|
|
478
|
+
fs3.rmSync(hooksDir, { recursive: true });
|
|
479
|
+
}
|
|
480
|
+
} catch (err) {
|
|
481
|
+
logger5.warn("Failed to cleanup hooks", { error: err.message });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
// Private
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
writeHookScripts(ctx) {
|
|
488
|
+
const hooksDir = path2.join(ctx.workDir, HOOKS_DIR);
|
|
489
|
+
fs3.mkdirSync(hooksDir, { recursive: true });
|
|
490
|
+
const eventsFile = path2.join(ctx.workDir, ".claude-plan", EVENTS_FILE_NAME);
|
|
491
|
+
const manifestFile = path2.join(ctx.workDir, ".claude-plan", MANIFEST_FILE_NAME);
|
|
492
|
+
const contextFile = path2.join(ctx.workDir, ".claude-plan", CONTEXT_FILE_NAME);
|
|
493
|
+
const expected = ctx.expectedArtifacts.join(",");
|
|
494
|
+
const phaseExpected = (ctx.phaseExpectedArtifacts ?? ctx.expectedArtifacts).join(",");
|
|
495
|
+
const scripts = [
|
|
496
|
+
{ name: "session-start.sh", content: buildSessionStartScript(eventsFile) },
|
|
497
|
+
{ name: "compact-restore.sh", content: buildCompactRestoreScript(eventsFile, contextFile) },
|
|
498
|
+
{ name: "post-tool-use.sh", content: buildPostToolUseScript(eventsFile, manifestFile, expected) },
|
|
499
|
+
{ name: "post-artifact.sh", content: buildPostArtifactScript(manifestFile, expected) },
|
|
500
|
+
{ name: "exit-plan-mode.sh", content: buildExitPlanModeScript(eventsFile) },
|
|
501
|
+
{ name: "permission.sh", content: buildPermissionScript(eventsFile) },
|
|
502
|
+
{ name: "protect-files.sh", content: buildProtectFilesScript(eventsFile, ctx.phaseName, ctx.planDir) },
|
|
503
|
+
{ name: "stop.sh", content: buildStopScript(eventsFile, ctx.planDir, phaseExpected) },
|
|
504
|
+
{ name: "ask-user-hook.sh", content: buildAskUserHookScript(eventsFile) },
|
|
505
|
+
{ name: "notification.sh", content: buildNotificationScript(eventsFile) }
|
|
506
|
+
];
|
|
507
|
+
for (const { name, content } of scripts) {
|
|
508
|
+
const scriptPath = path2.join(hooksDir, name);
|
|
509
|
+
fs3.writeFileSync(scriptPath, content, { mode: 493 });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
writeContextFile(ctx) {
|
|
513
|
+
const contextPath = path2.join(ctx.workDir, ".claude-plan", CONTEXT_FILE_NAME);
|
|
514
|
+
const context = {
|
|
515
|
+
issueIid: ctx.issueIid,
|
|
516
|
+
issueTitle: ctx.issueTitle ?? "",
|
|
517
|
+
issueDescription: ctx.issueDescription ?? "",
|
|
518
|
+
phaseName: ctx.phaseName ?? "",
|
|
519
|
+
expectedArtifacts: ctx.expectedArtifacts,
|
|
520
|
+
planDir: ctx.planDir
|
|
521
|
+
};
|
|
522
|
+
fs3.writeFileSync(contextPath, JSON.stringify(context, null, 2), "utf-8");
|
|
523
|
+
}
|
|
524
|
+
writeSettingsLocal(ctx) {
|
|
525
|
+
const claudeDir = path2.join(ctx.workDir, ".claude");
|
|
526
|
+
fs3.mkdirSync(claudeDir, { recursive: true });
|
|
527
|
+
const settingsPath = path2.join(claudeDir, "settings.local.json");
|
|
528
|
+
let existing = {};
|
|
529
|
+
if (fs3.existsSync(settingsPath)) {
|
|
530
|
+
try {
|
|
531
|
+
existing = JSON.parse(fs3.readFileSync(settingsPath, "utf-8"));
|
|
532
|
+
} catch {
|
|
533
|
+
logger5.warn("Failed to parse existing settings.local.json, overwriting");
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const hooksDir = path2.join(ctx.workDir, HOOKS_DIR);
|
|
537
|
+
const hooks = buildHooksConfig(hooksDir, ctx);
|
|
538
|
+
const merged = { ...existing, hooks };
|
|
539
|
+
fs3.writeFileSync(settingsPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
540
|
+
}
|
|
541
|
+
initEventsFile(ctx) {
|
|
542
|
+
const eventsPath = path2.join(ctx.workDir, ".claude-plan", EVENTS_FILE_NAME);
|
|
543
|
+
const manifestPath = path2.join(ctx.workDir, ".claude-plan", MANIFEST_FILE_NAME);
|
|
544
|
+
fs3.writeFileSync(eventsPath, "", "utf-8");
|
|
545
|
+
fs3.writeFileSync(manifestPath, "", "utf-8");
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
function buildHooksConfig(hooksDir, ctx) {
|
|
549
|
+
const isPlanPhase = ctx.phaseName === "plan";
|
|
550
|
+
const artifactIfPatterns = buildArtifactIfPatterns(ctx.expectedArtifacts);
|
|
551
|
+
const config = {
|
|
552
|
+
SessionStart: [
|
|
553
|
+
{
|
|
554
|
+
hooks: [{
|
|
555
|
+
type: "command",
|
|
556
|
+
command: path2.join(hooksDir, "session-start.sh"),
|
|
557
|
+
timeout: 5
|
|
558
|
+
}]
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
matcher: "compact",
|
|
562
|
+
hooks: [{
|
|
563
|
+
type: "command",
|
|
564
|
+
command: path2.join(hooksDir, "compact-restore.sh"),
|
|
565
|
+
timeout: 5
|
|
566
|
+
}]
|
|
567
|
+
}
|
|
568
|
+
],
|
|
569
|
+
PreToolUse: [
|
|
570
|
+
{
|
|
571
|
+
matcher: "AskUserQuestion",
|
|
572
|
+
hooks: [{
|
|
573
|
+
type: "command",
|
|
574
|
+
command: path2.join(hooksDir, "ask-user-hook.sh"),
|
|
575
|
+
timeout: 5
|
|
576
|
+
}]
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
matcher: "Edit|Write",
|
|
580
|
+
hooks: [{
|
|
581
|
+
type: "command",
|
|
582
|
+
command: path2.join(hooksDir, "protect-files.sh"),
|
|
583
|
+
timeout: 5,
|
|
584
|
+
...buildProtectIfClause(ctx.phaseName)
|
|
585
|
+
}]
|
|
586
|
+
}
|
|
587
|
+
],
|
|
588
|
+
PostToolUse: buildPostToolUseConfig(hooksDir, artifactIfPatterns),
|
|
589
|
+
PermissionRequest: buildPermissionRequestConfig(hooksDir, isPlanPhase),
|
|
590
|
+
Notification: [{
|
|
591
|
+
hooks: [{
|
|
592
|
+
type: "command",
|
|
593
|
+
command: path2.join(hooksDir, "notification.sh"),
|
|
594
|
+
timeout: 5
|
|
595
|
+
}]
|
|
596
|
+
}],
|
|
597
|
+
Stop: [{
|
|
598
|
+
hooks: [{
|
|
599
|
+
type: "command",
|
|
600
|
+
command: path2.join(hooksDir, "stop.sh"),
|
|
601
|
+
timeout: 15
|
|
602
|
+
}]
|
|
603
|
+
}]
|
|
604
|
+
};
|
|
605
|
+
return config;
|
|
606
|
+
}
|
|
607
|
+
function buildPermissionRequestConfig(hooksDir, isPlanPhase) {
|
|
608
|
+
const groups = [];
|
|
609
|
+
if (isPlanPhase) {
|
|
610
|
+
groups.push({
|
|
611
|
+
matcher: "ExitPlanMode",
|
|
612
|
+
hooks: [{
|
|
613
|
+
type: "command",
|
|
614
|
+
command: path2.join(hooksDir, "exit-plan-mode.sh"),
|
|
615
|
+
timeout: 5
|
|
616
|
+
}]
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
groups.push({
|
|
620
|
+
matcher: "Bash|Edit|Write|Read|Glob|Grep|WebFetch|WebSearch|mcp__.*",
|
|
621
|
+
hooks: [{
|
|
622
|
+
type: "command",
|
|
623
|
+
command: path2.join(hooksDir, "permission.sh"),
|
|
624
|
+
timeout: 5
|
|
625
|
+
}]
|
|
626
|
+
});
|
|
627
|
+
return groups;
|
|
628
|
+
}
|
|
629
|
+
function buildPostToolUseConfig(hooksDir, artifactIfPatterns) {
|
|
630
|
+
const groups = [];
|
|
631
|
+
if (artifactIfPatterns) {
|
|
632
|
+
groups.push({
|
|
633
|
+
matcher: "Write|Edit",
|
|
634
|
+
hooks: [{
|
|
635
|
+
type: "command",
|
|
636
|
+
command: path2.join(hooksDir, "post-artifact.sh"),
|
|
637
|
+
timeout: 10,
|
|
638
|
+
if: artifactIfPatterns
|
|
639
|
+
}]
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
groups.push({
|
|
643
|
+
matcher: "Write|Edit",
|
|
644
|
+
hooks: [{
|
|
645
|
+
type: "command",
|
|
646
|
+
command: path2.join(hooksDir, "post-tool-use.sh"),
|
|
647
|
+
timeout: 10
|
|
648
|
+
}]
|
|
649
|
+
});
|
|
650
|
+
return groups;
|
|
651
|
+
}
|
|
652
|
+
function buildArtifactIfPatterns(artifacts) {
|
|
653
|
+
if (artifacts.length === 0) return void 0;
|
|
654
|
+
return artifacts.flatMap((f) => [`Write(*${f})`, `Edit(*${f})`]).join("|");
|
|
655
|
+
}
|
|
656
|
+
function buildProtectIfClause(_phaseName) {
|
|
657
|
+
const alwaysProtected = [".env", ".env.*", "package-lock.json", "pnpm-lock.yaml"];
|
|
658
|
+
const ifValue = alwaysProtected.flatMap((f) => [`Edit(*${f})`, `Write(*${f})`]).join("|");
|
|
659
|
+
return { if: ifValue };
|
|
660
|
+
}
|
|
661
|
+
function buildSessionStartScript(eventsFile) {
|
|
662
|
+
return `#!/bin/bash
|
|
663
|
+
set -euo pipefail
|
|
664
|
+
INPUT=$(cat)
|
|
665
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
|
|
666
|
+
printf '{"ts":"%s","event":"session_start","session_id":"%s"}\\n' \\
|
|
667
|
+
"$(date -u +%FT%TZ)" "$SESSION_ID" >> ${quote(eventsFile)}
|
|
668
|
+
exit 0
|
|
669
|
+
`;
|
|
670
|
+
}
|
|
671
|
+
function buildCompactRestoreScript(eventsFile, contextFile) {
|
|
672
|
+
return `#!/bin/bash
|
|
673
|
+
set -euo pipefail
|
|
674
|
+
|
|
675
|
+
CONTEXT_FILE=${quote(contextFile)}
|
|
676
|
+
if [ ! -f "$CONTEXT_FILE" ]; then
|
|
677
|
+
exit 0
|
|
678
|
+
fi
|
|
679
|
+
|
|
680
|
+
ISSUE_IID=$(jq -r '.issueIid // empty' < "$CONTEXT_FILE")
|
|
681
|
+
ISSUE_TITLE=$(jq -r '.issueTitle // empty' < "$CONTEXT_FILE")
|
|
682
|
+
ISSUE_DESC=$(jq -r '.issueDescription // empty' < "$CONTEXT_FILE")
|
|
683
|
+
PHASE=$(jq -r '.phaseName // empty' < "$CONTEXT_FILE")
|
|
684
|
+
PLAN_DIR=$(jq -r '.planDir // empty' < "$CONTEXT_FILE")
|
|
685
|
+
ARTIFACTS=$(jq -r '.expectedArtifacts | join(", ") // empty' < "$CONTEXT_FILE")
|
|
686
|
+
|
|
687
|
+
READY=""
|
|
688
|
+
MISSING=""
|
|
689
|
+
for f in $(jq -r '.expectedArtifacts[]' < "$CONTEXT_FILE" 2>/dev/null); do
|
|
690
|
+
FPATH="$PLAN_DIR/$f"
|
|
691
|
+
if [ -f "$FPATH" ] && [ "$(wc -c < "$FPATH")" -ge 50 ]; then
|
|
692
|
+
READY="$READY $f"
|
|
693
|
+
else
|
|
694
|
+
MISSING="$MISSING $f"
|
|
695
|
+
fi
|
|
696
|
+
done
|
|
697
|
+
READY=$(echo "$READY" | xargs)
|
|
698
|
+
MISSING=$(echo "$MISSING" | xargs)
|
|
699
|
+
|
|
700
|
+
printf '{"ts":"%s","event":"compact_restore"}\\n' "$(date -u +%FT%TZ)" >> ${quote(eventsFile)}
|
|
701
|
+
|
|
702
|
+
cat <<CONTEXT
|
|
703
|
+
[\u4E0A\u4E0B\u6587\u6062\u590D \u2014 compaction \u540E\u81EA\u52A8\u6CE8\u5165]
|
|
704
|
+
Issue #$ISSUE_IID: $ISSUE_TITLE
|
|
705
|
+
\u5F53\u524D\u9636\u6BB5: $PHASE
|
|
706
|
+
\u9884\u671F\u4EA7\u7269: $ARTIFACTS
|
|
707
|
+
\u5DF2\u5C31\u7EEA: \${READY:-\u65E0}
|
|
708
|
+
\u672A\u5B8C\u6210: \${MISSING:-\u65E0}
|
|
709
|
+
|
|
710
|
+
\u9700\u6C42\u63CF\u8FF0:
|
|
711
|
+
$ISSUE_DESC
|
|
712
|
+
CONTEXT
|
|
713
|
+
exit 0
|
|
714
|
+
`;
|
|
715
|
+
}
|
|
716
|
+
function buildPostToolUseScript(eventsFile, manifestFile, expected) {
|
|
717
|
+
return `#!/bin/bash
|
|
718
|
+
set -euo pipefail
|
|
719
|
+
INPUT=$(cat)
|
|
720
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
721
|
+
[ -z "$FILE_PATH" ] && exit 0
|
|
722
|
+
|
|
723
|
+
EXPECTED=${quote(expected)}
|
|
724
|
+
BASENAME=$(basename "$FILE_PATH")
|
|
725
|
+
|
|
726
|
+
if echo "$EXPECTED" | tr ',' '\\n' | grep -qx "$BASENAME"; then
|
|
727
|
+
BYTES=$(wc -c < "$FILE_PATH" 2>/dev/null || echo 0)
|
|
728
|
+
printf '{"ts":"%s","event":"artifact_write","file":"%s","path":"%s","bytes":%s}\\n' \\
|
|
729
|
+
"$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" "$BYTES" >> ${quote(manifestFile)}
|
|
730
|
+
fi
|
|
731
|
+
|
|
732
|
+
printf '{"ts":"%s","event":"artifact_write","file":"%s","path":"%s","bytes":0}\\n' \\
|
|
733
|
+
"$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" >> ${quote(eventsFile)}
|
|
734
|
+
exit 0
|
|
735
|
+
`;
|
|
736
|
+
}
|
|
737
|
+
function buildPostArtifactScript(manifestFile, expected) {
|
|
738
|
+
return `#!/bin/bash
|
|
739
|
+
set -euo pipefail
|
|
740
|
+
INPUT=$(cat)
|
|
741
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
742
|
+
[ -z "$FILE_PATH" ] && exit 0
|
|
743
|
+
|
|
744
|
+
EXPECTED=${quote(expected)}
|
|
745
|
+
BASENAME=$(basename "$FILE_PATH")
|
|
746
|
+
|
|
747
|
+
if echo "$EXPECTED" | tr ',' '\\n' | grep -qx "$BASENAME"; then
|
|
748
|
+
BYTES=$(wc -c < "$FILE_PATH" 2>/dev/null || echo 0)
|
|
749
|
+
printf '{"ts":"%s","event":"write","file":"%s","path":"%s","bytes":%s}\\n' \\
|
|
750
|
+
"$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" "$BYTES" >> ${quote(manifestFile)}
|
|
751
|
+
fi
|
|
752
|
+
exit 0
|
|
753
|
+
`;
|
|
754
|
+
}
|
|
755
|
+
function buildExitPlanModeScript(eventsFile) {
|
|
756
|
+
return `#!/bin/bash
|
|
757
|
+
set -euo pipefail
|
|
758
|
+
INPUT=$(cat)
|
|
759
|
+
|
|
760
|
+
printf '{"ts":"%s","event":"exit_plan_mode"}\\n' "$(date -u +%FT%TZ)" >> ${quote(eventsFile)}
|
|
761
|
+
|
|
762
|
+
echo '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
|
|
763
|
+
exit 0
|
|
764
|
+
`;
|
|
765
|
+
}
|
|
766
|
+
function buildPermissionScript(eventsFile) {
|
|
767
|
+
return `#!/bin/bash
|
|
768
|
+
set -euo pipefail
|
|
769
|
+
INPUT=$(cat)
|
|
770
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
|
|
771
|
+
printf '{"ts":"%s","event":"permission_request","tool":"%s"}\\n' \\
|
|
772
|
+
"$(date -u +%FT%TZ)" "$TOOL" >> ${quote(eventsFile)}
|
|
773
|
+
echo '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
|
|
774
|
+
exit 0
|
|
775
|
+
`;
|
|
776
|
+
}
|
|
777
|
+
function buildProtectFilesScript(eventsFile, _phaseName, _planDir) {
|
|
778
|
+
return `#!/bin/bash
|
|
779
|
+
set -euo pipefail
|
|
780
|
+
INPUT=$(cat)
|
|
781
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
|
|
782
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
783
|
+
[ -z "$FILE_PATH" ] && exit 0
|
|
784
|
+
|
|
785
|
+
BASENAME=$(basename "$FILE_PATH")
|
|
786
|
+
|
|
787
|
+
blocked_reason() {
|
|
788
|
+
printf '{"ts":"%s","event":"protect_blocked","tool":"%s","file":"%s"}\\n' \\
|
|
789
|
+
"$(date -u +%FT%TZ)" "$TOOL" "$BASENAME" >> ${quote(eventsFile)}
|
|
790
|
+
echo "$1" >&2
|
|
791
|
+
exit 2
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
case "$BASENAME" in
|
|
795
|
+
.env|.env.*)
|
|
796
|
+
blocked_reason "\u7981\u6B62\u4FEE\u6539\u73AF\u5883\u914D\u7F6E\u6587\u4EF6 $BASENAME\uFF0C\u8BF7\u901A\u8FC7 .env.example \u6216\u6587\u6863\u8BF4\u660E\u914D\u7F6E\u53D8\u66F4\u3002"
|
|
797
|
+
;;
|
|
798
|
+
package-lock.json|pnpm-lock.yaml)
|
|
799
|
+
blocked_reason "\u7981\u6B62\u76F4\u63A5\u4FEE\u6539\u9501\u6587\u4EF6 $BASENAME\uFF0C\u8BF7\u901A\u8FC7 npm install / pnpm install \u66F4\u65B0\u4F9D\u8D56\u3002"
|
|
800
|
+
;;
|
|
801
|
+
esac
|
|
802
|
+
|
|
803
|
+
exit 0
|
|
804
|
+
`;
|
|
805
|
+
}
|
|
806
|
+
function buildStopScript(eventsFile, planDir, phaseExpected) {
|
|
807
|
+
return `#!/bin/bash
|
|
808
|
+
set -euo pipefail
|
|
809
|
+
INPUT=$(cat)
|
|
810
|
+
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
|
|
811
|
+
|
|
812
|
+
PLAN_DIR=${quote(planDir)}
|
|
813
|
+
MIN_BYTES=50
|
|
814
|
+
PHASE_EXPECTED=${quote(phaseExpected)}
|
|
815
|
+
|
|
816
|
+
MISSING=""
|
|
817
|
+
READY=""
|
|
818
|
+
for f in $(echo "$PHASE_EXPECTED" | tr ',' ' '); do
|
|
819
|
+
[ -z "$f" ] && continue
|
|
820
|
+
FPATH="$PLAN_DIR/$f"
|
|
821
|
+
if [ -f "$FPATH" ] && [ "$(wc -c < "$FPATH")" -ge "$MIN_BYTES" ]; then
|
|
822
|
+
BYTES=$(wc -c < "$FPATH")
|
|
823
|
+
READY="$READY $f(\${BYTES} bytes)"
|
|
824
|
+
else
|
|
825
|
+
MISSING="$MISSING $f"
|
|
826
|
+
fi
|
|
827
|
+
done
|
|
828
|
+
|
|
829
|
+
MISSING=$(echo "$MISSING" | xargs)
|
|
830
|
+
READY=$(echo "$READY" | xargs)
|
|
831
|
+
|
|
832
|
+
if [ -n "$MISSING" ] && [ "$STOP_ACTIVE" != "true" ]; then
|
|
833
|
+
printf '{"ts":"%s","event":"stop","blocked":true,"missing":"%s"}\\n' \\
|
|
834
|
+
"$(date -u +%FT%TZ)" "$MISSING" >> ${quote(eventsFile)}
|
|
835
|
+
|
|
836
|
+
REASON="\u4EA7\u7269\u672A\u5C31\u7EEA: $MISSING\u3002\u8BF7\u5199\u5165 $PLAN_DIR/ \u4E0B\u7684\u5BF9\u5E94\u6587\u4EF6\u3002\u5DF2\u5C31\u7EEA: \${READY:-\u65E0}"
|
|
837
|
+
|
|
838
|
+
printf '{"decision":"block","reason":"%s"}' "$REASON"
|
|
839
|
+
exit 0
|
|
840
|
+
fi
|
|
841
|
+
|
|
842
|
+
printf '{"ts":"%s","event":"stop","blocked":false,"missing":"%s"}\\n' \\
|
|
843
|
+
"$(date -u +%FT%TZ)" "\${MISSING:-none}" >> ${quote(eventsFile)}
|
|
844
|
+
exit 0
|
|
845
|
+
`;
|
|
846
|
+
}
|
|
847
|
+
function buildAskUserHookScript(eventsFile) {
|
|
848
|
+
return `#!/bin/bash
|
|
849
|
+
set -euo pipefail
|
|
850
|
+
INPUT=$(cat)
|
|
851
|
+
QUESTION=$(echo "$INPUT" | jq -r '.tool_input.question // empty')
|
|
852
|
+
OPTIONS=$(echo "$INPUT" | jq -c '[.tool_input.options[]? | {index: .index, label: .label}] // []' 2>/dev/null || echo '[]')
|
|
853
|
+
[ -z "$QUESTION" ] && exit 0
|
|
854
|
+
printf '{"ts":"%s","event":"ask_user_question","question":"%s","options":%s}\\n' \\
|
|
855
|
+
"$(date -u +%FT%TZ)" "$(echo "$QUESTION" | head -c 500 | tr '"' "'")" "$OPTIONS" >> ${quote(eventsFile)}
|
|
856
|
+
exit 0
|
|
857
|
+
`;
|
|
858
|
+
}
|
|
859
|
+
function buildNotificationScript(eventsFile) {
|
|
860
|
+
return `#!/bin/bash
|
|
861
|
+
set -euo pipefail
|
|
862
|
+
INPUT=$(cat)
|
|
863
|
+
NTYPE=$(echo "$INPUT" | jq -r '.notification_type // empty')
|
|
864
|
+
MSG=$(echo "$INPUT" | jq -r '.message // empty' | head -c 200 | tr '"' "'")
|
|
865
|
+
[ -z "$NTYPE" ] && exit 0
|
|
866
|
+
printf '{"ts":"%s","event":"notification","notification_type":"%s","message":"%s"}\\n' \\
|
|
867
|
+
"$(date -u +%FT%TZ)" "$NTYPE" "$MSG" >> ${quote(eventsFile)}
|
|
868
|
+
exit 0
|
|
869
|
+
`;
|
|
870
|
+
}
|
|
871
|
+
function quote(s) {
|
|
872
|
+
return `"${s.replace(/"/g, '\\"')}"`;
|
|
873
|
+
}
|
|
874
|
+
function readJsonl(filePath) {
|
|
875
|
+
if (!fs3.existsSync(filePath)) return [];
|
|
876
|
+
try {
|
|
877
|
+
const content = fs3.readFileSync(filePath, "utf-8").trim();
|
|
878
|
+
if (!content) return [];
|
|
879
|
+
return content.split("\n").reduce((acc, line) => {
|
|
880
|
+
const trimmed = line.trim();
|
|
881
|
+
if (!trimmed) return acc;
|
|
882
|
+
try {
|
|
883
|
+
acc.push(JSON.parse(trimmed));
|
|
884
|
+
} catch {
|
|
885
|
+
logger5.debug("Skipping malformed JSONL line", { line: trimmed });
|
|
886
|
+
}
|
|
887
|
+
return acc;
|
|
888
|
+
}, []);
|
|
889
|
+
} catch {
|
|
890
|
+
return [];
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
203
894
|
// src/ai-runner/PtyRunner.ts
|
|
204
|
-
var
|
|
895
|
+
var logger6 = logger.child("PtyRunner");
|
|
205
896
|
var ANSI_RE = /\x1b\[[?><=]*[0-9;]*[a-zA-Z~]|\x1b\][^\x07]*\x07|\x1b\(B/g;
|
|
206
897
|
function stripAnsi(str) {
|
|
207
898
|
return str.replace(ANSI_RE, "");
|
|
@@ -340,6 +1031,34 @@ function isTuiNoise(line) {
|
|
|
340
1031
|
if (CLAUDE_BANNER_INFO_RE.test(t)) return true;
|
|
341
1032
|
return false;
|
|
342
1033
|
}
|
|
1034
|
+
var InputWaitController = class {
|
|
1035
|
+
constructor(totalBudgetMs) {
|
|
1036
|
+
this.totalBudgetMs = totalBudgetMs;
|
|
1037
|
+
this.wallClockSegmentStart = Date.now();
|
|
1038
|
+
}
|
|
1039
|
+
_waiting = false;
|
|
1040
|
+
wallClockUsedMs = 0;
|
|
1041
|
+
wallClockSegmentStart;
|
|
1042
|
+
get waiting() {
|
|
1043
|
+
return this._waiting;
|
|
1044
|
+
}
|
|
1045
|
+
pause() {
|
|
1046
|
+
if (this._waiting) return;
|
|
1047
|
+
this._waiting = true;
|
|
1048
|
+
this.wallClockUsedMs += Date.now() - this.wallClockSegmentStart;
|
|
1049
|
+
}
|
|
1050
|
+
resume() {
|
|
1051
|
+
if (!this._waiting) return;
|
|
1052
|
+
this._waiting = false;
|
|
1053
|
+
this.wallClockSegmentStart = Date.now();
|
|
1054
|
+
}
|
|
1055
|
+
get remainingMs() {
|
|
1056
|
+
return Math.max(this.totalBudgetMs - this.wallClockUsedMs, 6e4);
|
|
1057
|
+
}
|
|
1058
|
+
get usedMs() {
|
|
1059
|
+
return this.wallClockUsedMs;
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
343
1062
|
var PtyRunner = class {
|
|
344
1063
|
constructor(nvmNodeVersion, terminalManager, defaultAgentMode, phaseAgentMap, globalModel, idleDetectMs = 3e4) {
|
|
345
1064
|
this.nvmNodeVersion = nvmNodeVersion;
|
|
@@ -357,32 +1076,32 @@ var PtyRunner = class {
|
|
|
357
1076
|
// ---- AIRunner interface ---------------------------------------------------
|
|
358
1077
|
async run(options) {
|
|
359
1078
|
if (isShuttingDown()) {
|
|
360
|
-
|
|
1079
|
+
logger6.warn("PtyRunner skipped \u2014 service is shutting down");
|
|
361
1080
|
return { success: false, output: "Service shutting down", exitCode: null };
|
|
362
1081
|
}
|
|
363
1082
|
const { prompt, workDir, timeoutMs, onStreamEvent, phaseName } = options;
|
|
364
1083
|
const agentMode = this.resolveAgentForPhase(phaseName);
|
|
365
1084
|
const startMode = options.mode;
|
|
366
1085
|
const continueSession = options.continueSession ?? false;
|
|
367
|
-
|
|
1086
|
+
logger6.info("PtyRunner.run()", { workDir, timeoutMs, phaseName, agentMode, continueSession });
|
|
368
1087
|
const { sessionId, isNew } = this.ensureSession(workDir, agentMode, startMode);
|
|
369
1088
|
if (isNew) {
|
|
370
|
-
|
|
1089
|
+
logger6.info("Waiting for AI agent prompt (new session)", { sessionId, phaseName });
|
|
371
1090
|
await this.waitForPrompt(sessionId, 3e5);
|
|
372
1091
|
} else if (continueSession) {
|
|
373
|
-
|
|
1092
|
+
logger6.info("Waiting for AI agent prompt (continue-session)", { sessionId, phaseName });
|
|
374
1093
|
await this.waitForPrompt(sessionId, 3e4);
|
|
375
1094
|
} else {
|
|
376
|
-
|
|
1095
|
+
logger6.info("Waiting for AI agent prompt (reused session)", { sessionId, phaseName });
|
|
377
1096
|
await this.waitForPrompt(sessionId, 1e4);
|
|
378
1097
|
}
|
|
379
1098
|
if (startMode === "plan" && this.shouldUseNativePlan(agentMode)) {
|
|
380
1099
|
return this.runNativePlanMode(sessionId, isNew, options, agentMode, workDir);
|
|
381
1100
|
}
|
|
382
1101
|
if (continueSession && !isNew) {
|
|
383
|
-
|
|
1102
|
+
logger6.info("Continue-session mode: attaching detectCompletion (no /clear)", { sessionId });
|
|
384
1103
|
const result2 = await this.detectCompletion(sessionId, options, onStreamEvent);
|
|
385
|
-
|
|
1104
|
+
logger6.info("PtyRunner continue-session completed", {
|
|
386
1105
|
workDir,
|
|
387
1106
|
agentMode,
|
|
388
1107
|
phaseName,
|
|
@@ -398,7 +1117,7 @@ var PtyRunner = class {
|
|
|
398
1117
|
const instruction = `Please read and follow all instructions in ${promptFile}`;
|
|
399
1118
|
await this.writeCommand(sessionId, instruction, agentMode);
|
|
400
1119
|
const result = await this.detectCompletion(sessionId, options, onStreamEvent);
|
|
401
|
-
|
|
1120
|
+
logger6.info("PtyRunner phase completed", {
|
|
402
1121
|
workDir,
|
|
403
1122
|
agentMode,
|
|
404
1123
|
phaseName,
|
|
@@ -413,7 +1132,7 @@ var PtyRunner = class {
|
|
|
413
1132
|
this.terminalManager.destroy(info.sessionId);
|
|
414
1133
|
}
|
|
415
1134
|
this.sessions.clear();
|
|
416
|
-
|
|
1135
|
+
logger6.info("PtyRunner: all managed sessions destroyed");
|
|
417
1136
|
}
|
|
418
1137
|
killByWorkDir(targetWorkDir) {
|
|
419
1138
|
const info = this.sessions.get(targetWorkDir);
|
|
@@ -432,7 +1151,7 @@ var PtyRunner = class {
|
|
|
432
1151
|
return false;
|
|
433
1152
|
}
|
|
434
1153
|
this.terminalManager.write(info.sessionId, "");
|
|
435
|
-
|
|
1154
|
+
logger6.info("Interrupted PTY session for retry", {
|
|
436
1155
|
workDir: targetWorkDir,
|
|
437
1156
|
sessionId: info.sessionId
|
|
438
1157
|
});
|
|
@@ -489,7 +1208,7 @@ var PtyRunner = class {
|
|
|
489
1208
|
ensureSession(workDir, agentMode, startMode) {
|
|
490
1209
|
const existing = this.sessions.get(workDir);
|
|
491
1210
|
if (existing && existing.agentMode !== agentMode) {
|
|
492
|
-
|
|
1211
|
+
logger6.info("Agent switched, destroying old PTY session", {
|
|
493
1212
|
workDir,
|
|
494
1213
|
oldAgent: existing.agentMode,
|
|
495
1214
|
newAgent: agentMode,
|
|
@@ -500,7 +1219,7 @@ var PtyRunner = class {
|
|
|
500
1219
|
}
|
|
501
1220
|
if (existing && existing.agentMode === agentMode) {
|
|
502
1221
|
if (this.terminalManager.get(existing.sessionId)) {
|
|
503
|
-
|
|
1222
|
+
logger6.info("Reusing existing PTY session (same agent)", {
|
|
504
1223
|
workDir,
|
|
505
1224
|
agentMode,
|
|
506
1225
|
sessionId: existing.sessionId
|
|
@@ -511,7 +1230,7 @@ var PtyRunner = class {
|
|
|
511
1230
|
}
|
|
512
1231
|
const orphan = this.terminalManager.findByWorkDir(workDir);
|
|
513
1232
|
if (orphan) {
|
|
514
|
-
|
|
1233
|
+
logger6.info("Destroying orphaned PTY session on workDir", {
|
|
515
1234
|
workDir,
|
|
516
1235
|
sessionId: orphan.id
|
|
517
1236
|
});
|
|
@@ -535,7 +1254,7 @@ var PtyRunner = class {
|
|
|
535
1254
|
currentMode: profile.defaultModeName ?? "bypass",
|
|
536
1255
|
startedWithMode: startMode
|
|
537
1256
|
});
|
|
538
|
-
|
|
1257
|
+
logger6.info("Created new PTY session", {
|
|
539
1258
|
workDir,
|
|
540
1259
|
agentMode,
|
|
541
1260
|
binary,
|
|
@@ -567,7 +1286,7 @@ var PtyRunner = class {
|
|
|
567
1286
|
if (stabilityTimer) clearTimeout(stabilityTimer);
|
|
568
1287
|
if (silenceTimer) clearTimeout(silenceTimer);
|
|
569
1288
|
subscription.dispose();
|
|
570
|
-
|
|
1289
|
+
logger6.warn("Timed out waiting for AI agent prompt", { sessionId, timeoutMs });
|
|
571
1290
|
resolve();
|
|
572
1291
|
}, timeoutMs);
|
|
573
1292
|
const done = (reason) => {
|
|
@@ -575,7 +1294,7 @@ var PtyRunner = class {
|
|
|
575
1294
|
if (stabilityTimer) clearTimeout(stabilityTimer);
|
|
576
1295
|
if (silenceTimer) clearTimeout(silenceTimer);
|
|
577
1296
|
subscription.dispose();
|
|
578
|
-
|
|
1297
|
+
logger6.info("AI agent prompt detected", { sessionId, reason });
|
|
579
1298
|
resolve();
|
|
580
1299
|
};
|
|
581
1300
|
const resetSilenceTimer = () => {
|
|
@@ -583,7 +1302,7 @@ var PtyRunner = class {
|
|
|
583
1302
|
if (silenceTimer) clearTimeout(silenceTimer);
|
|
584
1303
|
silenceTimer = setTimeout(() => {
|
|
585
1304
|
if (promptSeen) return;
|
|
586
|
-
|
|
1305
|
+
logger6.info("Banner shown and PTY silent \u2014 treating agent as ready", { sessionId });
|
|
587
1306
|
done("silence-after-banner");
|
|
588
1307
|
}, SILENCE_READY_MS);
|
|
589
1308
|
};
|
|
@@ -597,11 +1316,11 @@ var PtyRunner = class {
|
|
|
597
1316
|
resetSilenceTimer();
|
|
598
1317
|
if (!trustDialogHandled && TRUST_DIALOG_RE.test(stripped)) {
|
|
599
1318
|
trustDialogHandled = true;
|
|
600
|
-
|
|
1319
|
+
logger6.info("Trust dialog detected, auto-confirming", { sessionId });
|
|
601
1320
|
setTimeout(() => this.terminalManager.write(sessionId, "\r"), 500);
|
|
602
1321
|
}
|
|
603
1322
|
if (PERMISSION_DIALOG_RE.test(stripped)) {
|
|
604
|
-
|
|
1323
|
+
logger6.info("Permission dialog detected in waitForPrompt, auto-confirming", { sessionId });
|
|
605
1324
|
setTimeout(() => this.terminalManager.write(sessionId, "\r"), 500);
|
|
606
1325
|
}
|
|
607
1326
|
if (isIdlePrompt(stripped)) {
|
|
@@ -636,7 +1355,7 @@ var PtyRunner = class {
|
|
|
636
1355
|
if (!session) return;
|
|
637
1356
|
const targetMode = wantPlan ? profile.planModeName : profile.defaultModeName ?? "bypass";
|
|
638
1357
|
if (session.currentMode === targetMode) {
|
|
639
|
-
|
|
1358
|
+
logger6.info("PTY already in target mode", { sessionId, targetMode });
|
|
640
1359
|
return;
|
|
641
1360
|
}
|
|
642
1361
|
const MAX_ATTEMPTS = 5;
|
|
@@ -648,7 +1367,7 @@ var PtyRunner = class {
|
|
|
648
1367
|
if (detected) {
|
|
649
1368
|
session.currentMode = detected;
|
|
650
1369
|
if (detected === targetMode) {
|
|
651
|
-
|
|
1370
|
+
logger6.info("PTY mode switched", {
|
|
652
1371
|
sessionId,
|
|
653
1372
|
agentMode,
|
|
654
1373
|
targetMode,
|
|
@@ -658,7 +1377,7 @@ var PtyRunner = class {
|
|
|
658
1377
|
}
|
|
659
1378
|
}
|
|
660
1379
|
}
|
|
661
|
-
|
|
1380
|
+
logger6.warn("Failed to switch PTY mode after max attempts", {
|
|
662
1381
|
sessionId,
|
|
663
1382
|
agentMode,
|
|
664
1383
|
targetMode,
|
|
@@ -712,9 +1431,9 @@ var PtyRunner = class {
|
|
|
712
1431
|
const issueIid = extractIidFromPath(workDir);
|
|
713
1432
|
const contentHint = issueIid ? PlanFileResolver.buildContentHint(issueIid) : void 0;
|
|
714
1433
|
if (continueSession) {
|
|
715
|
-
|
|
1434
|
+
logger6.info("Native plan mode: continue-session (no /clear, no prompt)", { sessionId });
|
|
716
1435
|
} else {
|
|
717
|
-
|
|
1436
|
+
logger6.info("Native plan mode: switching to plan", { sessionId, agentMode });
|
|
718
1437
|
await this.ensurePlanMode(sessionId, agentMode, true, workDir);
|
|
719
1438
|
if (!isNew) {
|
|
720
1439
|
await this.writeCommand(sessionId, "/clear", agentMode);
|
|
@@ -734,16 +1453,16 @@ var PtyRunner = class {
|
|
|
734
1453
|
artifactCheck: planArtifactCheck
|
|
735
1454
|
}, options.onStreamEvent, continueSession);
|
|
736
1455
|
if (planResult.timedOut) {
|
|
737
|
-
|
|
1456
|
+
logger6.warn("Native plan mode: plan phase timed out", {
|
|
738
1457
|
sessionId,
|
|
739
1458
|
wasActive: planResult.wasActiveAtTimeout
|
|
740
1459
|
});
|
|
741
1460
|
return this.buildRunResult(planResult, sessionId);
|
|
742
1461
|
}
|
|
743
|
-
|
|
1462
|
+
logger6.info("Native plan mode: resolving plan file from CLI storage", { sessionId });
|
|
744
1463
|
const resolved = resolver.resolve(contentHint);
|
|
745
1464
|
if (!resolved) {
|
|
746
|
-
|
|
1465
|
+
logger6.error("Native plan mode: no plan file found in CLI storage", { sessionId, workDir });
|
|
747
1466
|
return {
|
|
748
1467
|
success: false,
|
|
749
1468
|
output: planResult.output,
|
|
@@ -755,26 +1474,26 @@ var PtyRunner = class {
|
|
|
755
1474
|
const artifactPaths = options.artifactPaths ?? [];
|
|
756
1475
|
if (artifactPaths.length > 0) {
|
|
757
1476
|
for (const targetPath of artifactPaths) {
|
|
758
|
-
const targetDir =
|
|
759
|
-
if (!
|
|
760
|
-
|
|
1477
|
+
const targetDir = path3.dirname(targetPath);
|
|
1478
|
+
if (!fs4.existsSync(targetDir)) {
|
|
1479
|
+
fs4.mkdirSync(targetDir, { recursive: true });
|
|
761
1480
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
source:
|
|
1481
|
+
fs4.writeFileSync(targetPath, resolved.content, "utf-8");
|
|
1482
|
+
logger6.info("Plan file copied to artifact path", {
|
|
1483
|
+
source: path3.basename(resolved.sourcePath),
|
|
765
1484
|
target: targetPath,
|
|
766
1485
|
size: resolved.content.length
|
|
767
1486
|
});
|
|
768
1487
|
}
|
|
769
1488
|
} else {
|
|
770
|
-
|
|
1489
|
+
logger6.warn("Native plan mode: no artifactPaths specified, plan file not copied", {
|
|
771
1490
|
sessionId,
|
|
772
1491
|
resolvedFile: resolved.sourcePath
|
|
773
1492
|
});
|
|
774
1493
|
}
|
|
775
|
-
|
|
1494
|
+
logger6.info("Native plan mode completed (deterministic copy)", {
|
|
776
1495
|
sessionId,
|
|
777
|
-
planSource:
|
|
1496
|
+
planSource: path3.basename(resolved.sourcePath),
|
|
778
1497
|
artifactsCopied: artifactPaths.length
|
|
779
1498
|
});
|
|
780
1499
|
return this.buildRunResult(planResult, sessionId);
|
|
@@ -792,13 +1511,19 @@ var PtyRunner = class {
|
|
|
792
1511
|
};
|
|
793
1512
|
}
|
|
794
1513
|
writePromptFile(workDir, prompt) {
|
|
795
|
-
const dir =
|
|
796
|
-
if (!
|
|
797
|
-
|
|
1514
|
+
const dir = path3.join(workDir, ".claude-plan");
|
|
1515
|
+
if (!fs4.existsSync(dir)) {
|
|
1516
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
1517
|
+
}
|
|
1518
|
+
const gitignorePath = path3.join(dir, ".gitignore");
|
|
1519
|
+
const entry = ".phase-prompt.md";
|
|
1520
|
+
if (!fs4.existsSync(gitignorePath) || !fs4.readFileSync(gitignorePath, "utf-8").includes(entry)) {
|
|
1521
|
+
fs4.appendFileSync(gitignorePath, `${entry}
|
|
1522
|
+
`, "utf-8");
|
|
798
1523
|
}
|
|
799
1524
|
const relPath = ".claude-plan/.phase-prompt.md";
|
|
800
|
-
const absPath =
|
|
801
|
-
|
|
1525
|
+
const absPath = path3.join(workDir, relPath);
|
|
1526
|
+
fs4.writeFileSync(absPath, prompt, "utf-8");
|
|
802
1527
|
return relPath;
|
|
803
1528
|
}
|
|
804
1529
|
// ---- Completion detection -------------------------------------------------
|
|
@@ -818,6 +1543,27 @@ var PtyRunner = class {
|
|
|
818
1543
|
let pendingDialogParsed = null;
|
|
819
1544
|
const idleTimeoutMs = options.idleTimeoutMs ?? 6e5;
|
|
820
1545
|
const timeoutMs = options.timeoutMs;
|
|
1546
|
+
const inputWait = new InputWaitController(timeoutMs);
|
|
1547
|
+
const pauseTimersForInput = () => {
|
|
1548
|
+
if (inputWait.waiting) return;
|
|
1549
|
+
inputWait.pause();
|
|
1550
|
+
clearTimeout(wallTimer);
|
|
1551
|
+
logger6.info("Timers paused \u2014 waiting for user input", {
|
|
1552
|
+
sessionId,
|
|
1553
|
+
wallClockUsedMs: inputWait.usedMs
|
|
1554
|
+
});
|
|
1555
|
+
};
|
|
1556
|
+
const resumeTimersAfterInput = () => {
|
|
1557
|
+
if (!inputWait.waiting) return;
|
|
1558
|
+
inputWait.resume();
|
|
1559
|
+
lastOutputTime = Date.now();
|
|
1560
|
+
wallTimer = scheduleWallTimer(inputWait.remainingMs);
|
|
1561
|
+
logger6.info("Timers resumed after user input", {
|
|
1562
|
+
sessionId,
|
|
1563
|
+
remainingMs: inputWait.remainingMs,
|
|
1564
|
+
wallClockUsedMs: inputWait.usedMs
|
|
1565
|
+
});
|
|
1566
|
+
};
|
|
821
1567
|
const GRACE_WINDOW_MS = options.timeoutGraceMs ?? 6e4;
|
|
822
1568
|
const EXTENSION_MS = options.timeoutExtensionMs ?? 6e5;
|
|
823
1569
|
const MAX_EXTENSIONS = options.timeoutMaxExtensions ?? 3;
|
|
@@ -830,11 +1576,12 @@ var PtyRunner = class {
|
|
|
830
1576
|
};
|
|
831
1577
|
const scheduleWallTimer = (delayMs) => {
|
|
832
1578
|
return setTimeout(() => {
|
|
1579
|
+
if (inputWait.waiting) return;
|
|
833
1580
|
const recentMs = Date.now() - lastOutputTime;
|
|
834
1581
|
const isActive = hasSubstantiveOutput && recentMs < GRACE_WINDOW_MS;
|
|
835
1582
|
if (isActive && extensions < MAX_EXTENSIONS) {
|
|
836
1583
|
extensions++;
|
|
837
|
-
|
|
1584
|
+
logger6.info("Wall-clock timeout extended (agent still active)", {
|
|
838
1585
|
sessionId,
|
|
839
1586
|
extensions,
|
|
840
1587
|
maxExtensions: MAX_EXTENSIONS,
|
|
@@ -853,6 +1600,7 @@ var PtyRunner = class {
|
|
|
853
1600
|
};
|
|
854
1601
|
let wallTimer = scheduleWallTimer(timeoutMs);
|
|
855
1602
|
const idleCheck = setInterval(() => {
|
|
1603
|
+
if (inputWait.waiting) return;
|
|
856
1604
|
if (!hasSubstantiveOutput) return;
|
|
857
1605
|
if (Date.now() - lastOutputTime >= idleTimeoutMs) {
|
|
858
1606
|
finish({
|
|
@@ -863,6 +1611,52 @@ var PtyRunner = class {
|
|
|
863
1611
|
});
|
|
864
1612
|
}
|
|
865
1613
|
}, 5e3);
|
|
1614
|
+
const dialogClassifier = new DialogClassifier();
|
|
1615
|
+
let hookWatcher;
|
|
1616
|
+
let hookSub;
|
|
1617
|
+
const hookInjector = new HookInjector();
|
|
1618
|
+
const eventsFilePath = hookInjector.getEventsFilePath(options.workDir);
|
|
1619
|
+
if (fs4.existsSync(eventsFilePath)) {
|
|
1620
|
+
hookWatcher = new HookEventWatcher(eventsFilePath);
|
|
1621
|
+
hookSub = hookWatcher.onEvent((event) => {
|
|
1622
|
+
dialogClassifier.ingestHookEvent(event);
|
|
1623
|
+
if (event.event === "ask_user_question" && options.onInputRequired && !dialogHandled && !resolved) {
|
|
1624
|
+
const askData = dialogClassifier.consumePendingAskUser();
|
|
1625
|
+
if (askData) {
|
|
1626
|
+
dialogHandled = true;
|
|
1627
|
+
dialogBuffer.length = 0;
|
|
1628
|
+
if (dialogQuiesceTimer) {
|
|
1629
|
+
clearTimeout(dialogQuiesceTimer);
|
|
1630
|
+
dialogQuiesceTimer = void 0;
|
|
1631
|
+
}
|
|
1632
|
+
pauseTimersForInput();
|
|
1633
|
+
logger6.info("AskUserQuestion detected via hook (high confidence, zero delay)", {
|
|
1634
|
+
sessionId,
|
|
1635
|
+
question: askData.question.slice(0, 80),
|
|
1636
|
+
optionCount: askData.options.length
|
|
1637
|
+
});
|
|
1638
|
+
options.onInputRequired({
|
|
1639
|
+
type: "interactive-dialog",
|
|
1640
|
+
content: askData.question,
|
|
1641
|
+
options: askData.options
|
|
1642
|
+
}).then((response) => {
|
|
1643
|
+
resumeTimersAfterInput();
|
|
1644
|
+
dialogHandled = false;
|
|
1645
|
+
if (!resolved && response) {
|
|
1646
|
+
this.terminalManager.write(sessionId, response + "\r");
|
|
1647
|
+
}
|
|
1648
|
+
}).catch((err) => {
|
|
1649
|
+
logger6.warn("onInputRequired callback failed (hook-based)", {
|
|
1650
|
+
error: err.message
|
|
1651
|
+
});
|
|
1652
|
+
resumeTimersAfterInput();
|
|
1653
|
+
dialogHandled = false;
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
hookWatcher.start();
|
|
1659
|
+
}
|
|
866
1660
|
const subscription = this.terminalManager.onData(sessionId, (data) => {
|
|
867
1661
|
if (resolved) return;
|
|
868
1662
|
const stripped = stripAnsi(data);
|
|
@@ -877,7 +1671,7 @@ var PtyRunner = class {
|
|
|
877
1671
|
clearTimeout(dialogQuiesceTimer);
|
|
878
1672
|
dialogQuiesceTimer = void 0;
|
|
879
1673
|
pendingDialogParsed = null;
|
|
880
|
-
|
|
1674
|
+
logger6.info("Dialog quiesce cancelled \u2014 new substantive output arrived", { sessionId });
|
|
881
1675
|
}
|
|
882
1676
|
}
|
|
883
1677
|
if (!echoConsumed && stripped.includes(".phase-prompt.md")) {
|
|
@@ -888,27 +1682,27 @@ var PtyRunner = class {
|
|
|
888
1682
|
return;
|
|
889
1683
|
}
|
|
890
1684
|
if (options.completionSignal && hasSubstantiveOutput && options.completionSignal.test(stripped)) {
|
|
891
|
-
|
|
1685
|
+
logger6.info("Completion signal detected", { sessionId });
|
|
892
1686
|
finish({ output: outputLines.join(""), timedOut: false });
|
|
893
1687
|
return;
|
|
894
1688
|
}
|
|
895
1689
|
if (hasSubstantiveOutput && WORKED_SUMMARY_RE.test(stripped)) {
|
|
896
1690
|
const artifactReady = options.artifactCheck ? options.artifactCheck() : true;
|
|
897
1691
|
if (artifactReady) {
|
|
898
|
-
|
|
1692
|
+
logger6.info("Session-end summary detected, finishing", { sessionId });
|
|
899
1693
|
finish({ output: outputLines.join(""), timedOut: false });
|
|
900
1694
|
return;
|
|
901
1695
|
}
|
|
902
|
-
|
|
1696
|
+
logger6.info("Session summary detected but artifacts not ready, continuing", { sessionId });
|
|
903
1697
|
}
|
|
904
1698
|
if (PERMISSION_DIALOG_RE.test(stripped)) {
|
|
905
|
-
|
|
1699
|
+
logger6.info("Permission dialog detected, auto-confirming", { sessionId });
|
|
906
1700
|
setTimeout(() => {
|
|
907
1701
|
if (!resolved) this.terminalManager.write(sessionId, "\r");
|
|
908
1702
|
}, 500);
|
|
909
1703
|
return;
|
|
910
1704
|
}
|
|
911
|
-
if (options.onInputRequired && !dialogHandled && isInteractiveDialog(stripped)) {
|
|
1705
|
+
if (options.onInputRequired && !dialogHandled && !dialogClassifier.shouldSuppressRegex() && isInteractiveDialog(stripped)) {
|
|
912
1706
|
const parsed = parseInteractiveDialog(stripped);
|
|
913
1707
|
if (parsed) {
|
|
914
1708
|
const confidence = getDialogConfidence(stripped);
|
|
@@ -919,7 +1713,8 @@ var PtyRunner = class {
|
|
|
919
1713
|
clearTimeout(dialogQuiesceTimer);
|
|
920
1714
|
dialogQuiesceTimer = void 0;
|
|
921
1715
|
}
|
|
922
|
-
|
|
1716
|
+
pauseTimersForInput();
|
|
1717
|
+
logger6.info("Interactive dialog detected (high confidence), forwarding to handler", {
|
|
923
1718
|
sessionId,
|
|
924
1719
|
question: parsed.question.slice(0, 80),
|
|
925
1720
|
optionCount: parsed.options.length
|
|
@@ -929,21 +1724,23 @@ var PtyRunner = class {
|
|
|
929
1724
|
content: parsed.question,
|
|
930
1725
|
options: parsed.options
|
|
931
1726
|
}).then((response) => {
|
|
1727
|
+
resumeTimersAfterInput();
|
|
932
1728
|
dialogHandled = false;
|
|
933
1729
|
if (!resolved && response) {
|
|
934
1730
|
this.terminalManager.write(sessionId, response + "\r");
|
|
935
1731
|
}
|
|
936
1732
|
}).catch((err) => {
|
|
937
|
-
|
|
1733
|
+
logger6.warn("onInputRequired callback failed for interactive dialog", {
|
|
938
1734
|
error: err.message
|
|
939
1735
|
});
|
|
1736
|
+
resumeTimersAfterInput();
|
|
940
1737
|
dialogHandled = false;
|
|
941
1738
|
});
|
|
942
1739
|
return;
|
|
943
1740
|
}
|
|
944
1741
|
if (!dialogQuiesceTimer) {
|
|
945
1742
|
pendingDialogParsed = parsed;
|
|
946
|
-
|
|
1743
|
+
logger6.info("Interactive dialog detected (low confidence), starting quiesce", {
|
|
947
1744
|
sessionId,
|
|
948
1745
|
question: parsed.question.slice(0, 80),
|
|
949
1746
|
optionCount: parsed.options.length
|
|
@@ -955,7 +1752,8 @@ var PtyRunner = class {
|
|
|
955
1752
|
dialogBuffer.length = 0;
|
|
956
1753
|
const dp = pendingDialogParsed;
|
|
957
1754
|
pendingDialogParsed = null;
|
|
958
|
-
|
|
1755
|
+
pauseTimersForInput();
|
|
1756
|
+
logger6.info("Dialog quiesce elapsed \u2014 forwarding low-confidence dialog", {
|
|
959
1757
|
sessionId,
|
|
960
1758
|
question: dp.question.slice(0, 80)
|
|
961
1759
|
});
|
|
@@ -964,14 +1762,16 @@ var PtyRunner = class {
|
|
|
964
1762
|
content: dp.question,
|
|
965
1763
|
options: dp.options
|
|
966
1764
|
}).then((response) => {
|
|
1765
|
+
resumeTimersAfterInput();
|
|
967
1766
|
dialogHandled = false;
|
|
968
1767
|
if (!resolved && response) {
|
|
969
1768
|
this.terminalManager.write(sessionId, response + "\r");
|
|
970
1769
|
}
|
|
971
1770
|
}).catch((err) => {
|
|
972
|
-
|
|
1771
|
+
logger6.warn("onInputRequired callback failed (quiesced dialog)", {
|
|
973
1772
|
error: err.message
|
|
974
1773
|
});
|
|
1774
|
+
resumeTimersAfterInput();
|
|
975
1775
|
dialogHandled = false;
|
|
976
1776
|
});
|
|
977
1777
|
}, DIALOG_QUIESCE_MS);
|
|
@@ -1005,10 +1805,10 @@ var PtyRunner = class {
|
|
|
1005
1805
|
if (hasSubstantiveOutput && isIdlePrompt(stripped)) {
|
|
1006
1806
|
if (isMixedFrame) {
|
|
1007
1807
|
} else if (options.completionSignal) {
|
|
1008
|
-
|
|
1808
|
+
logger6.debug("Idle prompt ignored (waiting for completionSignal)", { sessionId });
|
|
1009
1809
|
} else if (debounceTimer && isNoise) {
|
|
1010
1810
|
} else {
|
|
1011
|
-
if (options.onInputRequired && !dialogHandled && dialogBuffer.length >= 2) {
|
|
1811
|
+
if (options.onInputRequired && !dialogHandled && !dialogClassifier.shouldSuppressRegex() && dialogBuffer.length >= 2) {
|
|
1012
1812
|
const combined = dialogBuffer.join("\n");
|
|
1013
1813
|
if (isInteractiveDialog(combined)) {
|
|
1014
1814
|
const parsed = parseInteractiveDialog(combined);
|
|
@@ -1021,7 +1821,8 @@ var PtyRunner = class {
|
|
|
1021
1821
|
clearTimeout(dialogQuiesceTimer);
|
|
1022
1822
|
dialogQuiesceTimer = void 0;
|
|
1023
1823
|
}
|
|
1024
|
-
|
|
1824
|
+
pauseTimersForInput();
|
|
1825
|
+
logger6.info("Interactive dialog detected via accumulated buffer (high confidence)", {
|
|
1025
1826
|
sessionId,
|
|
1026
1827
|
question: parsed.question.slice(0, 80),
|
|
1027
1828
|
optionCount: parsed.options.length
|
|
@@ -1031,21 +1832,23 @@ var PtyRunner = class {
|
|
|
1031
1832
|
content: parsed.question,
|
|
1032
1833
|
options: parsed.options
|
|
1033
1834
|
}).then((response) => {
|
|
1835
|
+
resumeTimersAfterInput();
|
|
1034
1836
|
dialogHandled = false;
|
|
1035
1837
|
if (!resolved && response) {
|
|
1036
1838
|
this.terminalManager.write(sessionId, response + "\r");
|
|
1037
1839
|
}
|
|
1038
1840
|
}).catch((err) => {
|
|
1039
|
-
|
|
1841
|
+
logger6.warn("onInputRequired callback failed (accumulated)", {
|
|
1040
1842
|
error: err.message
|
|
1041
1843
|
});
|
|
1844
|
+
resumeTimersAfterInput();
|
|
1042
1845
|
dialogHandled = false;
|
|
1043
1846
|
});
|
|
1044
1847
|
return;
|
|
1045
1848
|
}
|
|
1046
1849
|
if (!dialogQuiesceTimer) {
|
|
1047
1850
|
pendingDialogParsed = parsed;
|
|
1048
|
-
|
|
1851
|
+
logger6.info("Dialog detected via buffer (low confidence), starting quiesce", {
|
|
1049
1852
|
sessionId,
|
|
1050
1853
|
question: parsed.question.slice(0, 80)
|
|
1051
1854
|
});
|
|
@@ -1056,20 +1859,23 @@ var PtyRunner = class {
|
|
|
1056
1859
|
dialogBuffer.length = 0;
|
|
1057
1860
|
const dp = pendingDialogParsed;
|
|
1058
1861
|
pendingDialogParsed = null;
|
|
1059
|
-
|
|
1862
|
+
pauseTimersForInput();
|
|
1863
|
+
logger6.info("Buffer dialog quiesce elapsed \u2014 forwarding", { sessionId });
|
|
1060
1864
|
options.onInputRequired({
|
|
1061
1865
|
type: "interactive-dialog",
|
|
1062
1866
|
content: dp.question,
|
|
1063
1867
|
options: dp.options
|
|
1064
1868
|
}).then((response) => {
|
|
1869
|
+
resumeTimersAfterInput();
|
|
1065
1870
|
dialogHandled = false;
|
|
1066
1871
|
if (!resolved && response) {
|
|
1067
1872
|
this.terminalManager.write(sessionId, response + "\r");
|
|
1068
1873
|
}
|
|
1069
1874
|
}).catch((err) => {
|
|
1070
|
-
|
|
1875
|
+
logger6.warn("onInputRequired callback failed (buffer quiesced)", {
|
|
1071
1876
|
error: err.message
|
|
1072
1877
|
});
|
|
1878
|
+
resumeTimersAfterInput();
|
|
1073
1879
|
dialogHandled = false;
|
|
1074
1880
|
});
|
|
1075
1881
|
}, DIALOG_QUIESCE_MS);
|
|
@@ -1088,7 +1894,7 @@ var PtyRunner = class {
|
|
|
1088
1894
|
}
|
|
1089
1895
|
const artifactReady = options.artifactCheck ? options.artifactCheck() : true;
|
|
1090
1896
|
if (!artifactReady) {
|
|
1091
|
-
|
|
1897
|
+
logger6.info("Idle prompt detected but artifacts not ready, continuing to wait", {
|
|
1092
1898
|
sessionId
|
|
1093
1899
|
});
|
|
1094
1900
|
scheduleDebounce();
|
|
@@ -1114,14 +1920,14 @@ var PtyRunner = class {
|
|
|
1114
1920
|
}
|
|
1115
1921
|
}
|
|
1116
1922
|
if (!resolved) {
|
|
1117
|
-
|
|
1923
|
+
logger6.warn("PTY process exited during phase", { sessionId, exitCode, wasKilled });
|
|
1118
1924
|
finish({
|
|
1119
1925
|
output: outputLines.join(""),
|
|
1120
1926
|
timedOut: wasKilled,
|
|
1121
1927
|
timeoutType: wasKilled ? "wall-clock" : void 0
|
|
1122
1928
|
});
|
|
1123
1929
|
} else {
|
|
1124
|
-
|
|
1930
|
+
logger6.info("PTY exited after phase completion (post stop-hook)", { sessionId, exitCode });
|
|
1125
1931
|
}
|
|
1126
1932
|
});
|
|
1127
1933
|
const cleanup = () => {
|
|
@@ -1130,6 +1936,9 @@ var PtyRunner = class {
|
|
|
1130
1936
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1131
1937
|
if (dialogQuiesceTimer) clearTimeout(dialogQuiesceTimer);
|
|
1132
1938
|
subscription.dispose();
|
|
1939
|
+
hookSub?.dispose();
|
|
1940
|
+
hookWatcher?.stop();
|
|
1941
|
+
dialogClassifier.reset();
|
|
1133
1942
|
};
|
|
1134
1943
|
});
|
|
1135
1944
|
}
|
|
@@ -1137,6 +1946,7 @@ var PtyRunner = class {
|
|
|
1137
1946
|
|
|
1138
1947
|
export {
|
|
1139
1948
|
PlanFileResolver,
|
|
1949
|
+
HookInjector,
|
|
1140
1950
|
stripAnsi,
|
|
1141
1951
|
isIdlePrompt,
|
|
1142
1952
|
TRUST_DIALOG_RE,
|
|
@@ -1148,6 +1958,7 @@ export {
|
|
|
1148
1958
|
isInteractiveDialog,
|
|
1149
1959
|
containsActiveWork,
|
|
1150
1960
|
isTuiNoise,
|
|
1961
|
+
InputWaitController,
|
|
1151
1962
|
PtyRunner
|
|
1152
1963
|
};
|
|
1153
|
-
//# sourceMappingURL=chunk-
|
|
1964
|
+
//# sourceMappingURL=chunk-HD6V7KPE.js.map
|