@xdevops/issue-auto-finish 1.0.92 → 1.0.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/{PtyRunner-NYASBTRP.js → PtyRunner-6RSDKUMM.js} +4 -2
  2. package/dist/ai-runner/DialogClassifier.d.ts +44 -0
  3. package/dist/ai-runner/DialogClassifier.d.ts.map +1 -0
  4. package/dist/ai-runner/PlanFileResolver.d.ts +1 -0
  5. package/dist/ai-runner/PlanFileResolver.d.ts.map +1 -1
  6. package/dist/ai-runner/PtyRunner.d.ts +17 -0
  7. package/dist/ai-runner/PtyRunner.d.ts.map +1 -1
  8. package/dist/{ai-runner-TOHVJJ76.js → ai-runner-45IRCBIR.js} +2 -2
  9. package/dist/{analyze-DBH4K3J7.js → analyze-7TY5DYBT.js} +2 -2
  10. package/dist/{braindump-RYI4BGMG.js → braindump-FLX6HEVB.js} +2 -2
  11. package/dist/{chunk-4XMYOXGZ.js → chunk-36G3DPO3.js} +944 -93
  12. package/dist/chunk-36G3DPO3.js.map +1 -0
  13. package/dist/{chunk-6T7ZHAV2.js → chunk-4JI5AJEA.js} +9 -9
  14. package/dist/{chunk-WZGEYHCC.js → chunk-MTXTSSBH.js} +271 -716
  15. package/dist/chunk-MTXTSSBH.js.map +1 -0
  16. package/dist/{chunk-ENF24C44.js → chunk-RR65A7J4.js} +2 -2
  17. package/dist/{chunk-2WDVTLVF.js → chunk-ZDY5NCP3.js} +1 -1
  18. package/dist/cli.js +5 -5
  19. package/dist/hooks/HookInjector.d.ts +20 -0
  20. package/dist/hooks/HookInjector.d.ts.map +1 -1
  21. package/dist/index.js +4 -4
  22. package/dist/{init-UKTP7LXS.js → init-O7XJLCP3.js} +2 -2
  23. package/dist/lib.js +2 -2
  24. package/dist/{restart-5D3ZDD5L.js → restart-4LNDGOOU.js} +2 -2
  25. package/dist/run.js +4 -4
  26. package/dist/{start-IQBNXLEI.js → start-Z4ODDTJ5.js} +2 -2
  27. package/package.json +1 -1
  28. package/src/web/frontend/dist/assets/index-BrvoaFSK.css +1 -0
  29. package/src/web/frontend/dist/assets/{index-BR0UoQER.js → index-CmyxgdS_.js} +54 -54
  30. package/src/web/frontend/dist/index.html +2 -2
  31. package/dist/chunk-4XMYOXGZ.js.map +0 -1
  32. package/dist/chunk-WZGEYHCC.js.map +0 -1
  33. package/src/web/frontend/dist/assets/index-DWOHf3bd.css +0 -1
  34. /package/dist/{PtyRunner-NYASBTRP.js.map → PtyRunner-6RSDKUMM.js.map} +0 -0
  35. /package/dist/{ai-runner-TOHVJJ76.js.map → ai-runner-45IRCBIR.js.map} +0 -0
  36. /package/dist/{analyze-DBH4K3J7.js.map → analyze-7TY5DYBT.js.map} +0 -0
  37. /package/dist/{braindump-RYI4BGMG.js.map → braindump-FLX6HEVB.js.map} +0 -0
  38. /package/dist/{chunk-6T7ZHAV2.js.map → chunk-4JI5AJEA.js.map} +0 -0
  39. /package/dist/{chunk-ENF24C44.js.map → chunk-RR65A7J4.js.map} +0 -0
  40. /package/dist/{chunk-2WDVTLVF.js.map → chunk-ZDY5NCP3.js.map} +0 -0
  41. /package/dist/{init-UKTP7LXS.js.map → init-O7XJLCP3.js.map} +0 -0
  42. /package/dist/{restart-5D3ZDD5L.js.map → restart-4LNDGOOU.js.map} +0 -0
  43. /package/dist/{start-IQBNXLEI.js.map → start-Z4ODDTJ5.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 fs2 from "fs";
16
- import path2 from "path";
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";
@@ -24,6 +24,9 @@ var PLAN_DIRS = {
24
24
  "claude-internal": path.join(os.homedir(), ".claude-internal", "plans"),
25
25
  "codebuddy": path.join(os.homedir(), ".codebuddy", "plans")
26
26
  };
27
+ var PLAN_METADATA_PATTERNS = [
28
+ /^00-plan-status\.md$/
29
+ ];
27
30
  var PlanFileResolver = class _PlanFileResolver {
28
31
  plansDir;
29
32
  beforeFiles = /* @__PURE__ */ new Map();
@@ -82,6 +85,7 @@ var PlanFileResolver = class _PlanFileResolver {
82
85
  });
83
86
  return this.fallbackByMtime(afterFiles, contentHint);
84
87
  }
88
+ candidates.sort((a, b) => b.mtime - a.mtime);
85
89
  if (candidates.length === 1) {
86
90
  return this.readCandidate(candidates[0].path, candidates[0].mtime);
87
91
  }
@@ -89,7 +93,6 @@ var PlanFileResolver = class _PlanFileResolver {
89
93
  const matched = this.matchByContent(candidates, contentHint);
90
94
  if (matched) return matched;
91
95
  }
92
- candidates.sort((a, b) => b.mtime - a.mtime);
93
96
  logger2.info("Multiple new plan files found, using most recent", {
94
97
  count: candidates.length,
95
98
  selected: path.basename(candidates[0].path)
@@ -128,12 +131,16 @@ var PlanFileResolver = class _PlanFileResolver {
128
131
  findNewFiles(afterFiles) {
129
132
  const candidates = [];
130
133
  for (const [filePath, mtime] of afterFiles) {
131
- if (!this.beforeFiles.has(filePath)) {
134
+ if (!this.beforeFiles.has(filePath) && !this.isMetadataFile(filePath)) {
132
135
  candidates.push({ path: filePath, mtime });
133
136
  }
134
137
  }
135
138
  return candidates;
136
139
  }
140
+ isMetadataFile(filePath) {
141
+ const basename = path.basename(filePath);
142
+ return PLAN_METADATA_PATTERNS.some((pattern) => pattern.test(basename));
143
+ }
137
144
  /**
138
145
  * Fallback: if no new files found (rare case — plan might have overwritten
139
146
  * an existing file), find the most recently modified file.
@@ -164,21 +171,27 @@ var PlanFileResolver = class _PlanFileResolver {
164
171
  return this.readCandidate(newestPath, newestMtime);
165
172
  }
166
173
  matchByContent(candidates, contentHint) {
174
+ const matches = [];
167
175
  for (const candidate of candidates) {
168
176
  try {
169
177
  const content = fs.readFileSync(candidate.path, "utf-8");
170
178
  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 };
179
+ matches.push({ sourcePath: candidate.path, content });
176
180
  }
177
181
  } catch {
178
182
  continue;
179
183
  }
180
184
  }
181
- return null;
185
+ if (matches.length === 0) return null;
186
+ matches.sort((a, b) => b.content.length - a.content.length);
187
+ const best = matches[0];
188
+ logger2.info("Plan file matched by content", {
189
+ file: path.basename(best.sourcePath),
190
+ hint: contentHint.slice(0, 50),
191
+ matchCount: matches.length,
192
+ size: best.content.length
193
+ });
194
+ return best;
182
195
  }
183
196
  contentMatches(content, hint) {
184
197
  const parts = hint.split("|");
@@ -200,8 +213,711 @@ var PlanFileResolver = class _PlanFileResolver {
200
213
  }
201
214
  };
202
215
 
216
+ // src/ai-runner/DialogClassifier.ts
217
+ var logger3 = logger.child("DialogClassifier");
218
+ var HOOK_EVENT_TTL_MS = 5e3;
219
+ var PERMISSION_SUPPRESS_MS = 2e3;
220
+ var ASK_USER_SUPPRESS_MS = 3e3;
221
+ var DialogClassifier = class {
222
+ recentEvents = [];
223
+ handledIds = /* @__PURE__ */ new Set();
224
+ ingestHookEvent(event) {
225
+ this.recentEvents.push({ event, receivedAt: Date.now() });
226
+ this.pruneExpired();
227
+ if (event.event === "ask_user_question") {
228
+ logger3.info("Received ask_user_question hook event", {
229
+ question: event.question.slice(0, 80),
230
+ optionCount: event.options.length
231
+ });
232
+ } else if (event.event === "notification") {
233
+ logger3.debug("Received notification hook event", {
234
+ type: event.notification_type
235
+ });
236
+ }
237
+ }
238
+ /**
239
+ * Check if there's a pending AskUserQuestion from hooks that hasn't been
240
+ * handled yet. Returns structured data or null.
241
+ */
242
+ consumePendingAskUser() {
243
+ const now = Date.now();
244
+ for (let i = this.recentEvents.length - 1; i >= 0; i--) {
245
+ const { event, receivedAt } = this.recentEvents[i];
246
+ if (event.event !== "ask_user_question") continue;
247
+ if (now - receivedAt > HOOK_EVENT_TTL_MS) continue;
248
+ const eventId = `ask_user_${event.ts}`;
249
+ if (this.handledIds.has(eventId)) continue;
250
+ this.handledIds.add(eventId);
251
+ return {
252
+ question: event.question,
253
+ options: mapHookOptions(event.options)
254
+ };
255
+ }
256
+ return null;
257
+ }
258
+ /**
259
+ * Whether a recent permission_prompt notification exists, indicating
260
+ * that regex-detected dialogs are likely false positives.
261
+ */
262
+ hasRecentPermissionPrompt() {
263
+ const now = Date.now();
264
+ return this.recentEvents.some(
265
+ ({ event, receivedAt }) => event.event === "notification" && event.notification_type === "permission_prompt" && now - receivedAt < PERMISSION_SUPPRESS_MS
266
+ );
267
+ }
268
+ /**
269
+ * Whether a recent ask_user_question hook event exists, indicating
270
+ * that regex detection should be suppressed to avoid duplicate forwarding.
271
+ */
272
+ hasRecentAskUser() {
273
+ const now = Date.now();
274
+ return this.recentEvents.some(
275
+ ({ event, receivedAt }) => event.event === "ask_user_question" && now - receivedAt < ASK_USER_SUPPRESS_MS
276
+ );
277
+ }
278
+ /**
279
+ * Whether regex-based dialog detection should be suppressed
280
+ * (either due to recent hook-based AskUser or permission_prompt).
281
+ */
282
+ shouldSuppressRegex() {
283
+ return this.hasRecentAskUser() || this.hasRecentPermissionPrompt();
284
+ }
285
+ reset() {
286
+ this.recentEvents.length = 0;
287
+ this.handledIds.clear();
288
+ }
289
+ pruneExpired() {
290
+ const cutoff = Date.now() - HOOK_EVENT_TTL_MS;
291
+ this.recentEvents = this.recentEvents.filter((e) => e.receivedAt > cutoff);
292
+ }
293
+ };
294
+ function mapHookOptions(hookOptions) {
295
+ return hookOptions.map((o, i) => ({
296
+ index: o.index ?? i + 1,
297
+ label: o.label
298
+ }));
299
+ }
300
+
301
+ // src/hooks/HookEventWatcher.ts
302
+ import fs2 from "fs";
303
+ var logger4 = logger.child("HookEventWatcher");
304
+ var HookEventWatcher = class {
305
+ eventsFile;
306
+ watcher = null;
307
+ pollTimer = null;
308
+ offset = 0;
309
+ listeners = [];
310
+ started = false;
311
+ constructor(eventsFile) {
312
+ this.eventsFile = eventsFile;
313
+ }
314
+ start() {
315
+ if (this.started) return;
316
+ this.started = true;
317
+ this.offset = this.getCurrentSize();
318
+ try {
319
+ this.watcher = fs2.watch(this.eventsFile, () => this.readNewEvents());
320
+ } catch {
321
+ logger4.debug("fs.watch unavailable, using poll-only mode");
322
+ }
323
+ this.pollTimer = setInterval(() => this.readNewEvents(), 1e3);
324
+ }
325
+ stop() {
326
+ if (!this.started) return;
327
+ this.started = false;
328
+ this.watcher?.close();
329
+ this.watcher = null;
330
+ if (this.pollTimer) {
331
+ clearInterval(this.pollTimer);
332
+ this.pollTimer = null;
333
+ }
334
+ this.listeners = [];
335
+ }
336
+ onEvent(callback) {
337
+ this.listeners.push(callback);
338
+ return {
339
+ dispose: () => {
340
+ this.listeners = this.listeners.filter((l) => l !== callback);
341
+ }
342
+ };
343
+ }
344
+ /**
345
+ * 等待指定类型的事件,带超时。
346
+ * 对于 'stop' 事件,只有 blocked=false 时才 resolve。
347
+ */
348
+ waitForEvent(eventType, timeoutMs) {
349
+ return new Promise((resolve, reject) => {
350
+ const timer = setTimeout(() => {
351
+ sub.dispose();
352
+ reject(new Error(`Timeout waiting for hook event "${eventType}" after ${timeoutMs}ms`));
353
+ }, timeoutMs);
354
+ const sub = this.onEvent((ev) => {
355
+ if (ev.event !== eventType) return;
356
+ if (ev.event === "stop" && ev.blocked) return;
357
+ clearTimeout(timer);
358
+ sub.dispose();
359
+ resolve(ev);
360
+ });
361
+ });
362
+ }
363
+ /** 获取已记录的所有产物写入事件摘要 */
364
+ getArtifactSummary() {
365
+ const events = this.readAll().filter(
366
+ (e) => e.event === "artifact_write"
367
+ );
368
+ if (events.length === 0) return "";
369
+ return events.map((e) => `${e.file} (${e.bytes} bytes)`).join(", ");
370
+ }
371
+ // ---------------------------------------------------------------------------
372
+ // Private
373
+ // ---------------------------------------------------------------------------
374
+ readNewEvents() {
375
+ if (!this.started) return;
376
+ const size = this.getCurrentSize();
377
+ if (size <= this.offset) return;
378
+ try {
379
+ const fd = fs2.openSync(this.eventsFile, "r");
380
+ try {
381
+ const buf = Buffer.alloc(size - this.offset);
382
+ fs2.readSync(fd, buf, 0, buf.length, this.offset);
383
+ this.offset = size;
384
+ const chunk = buf.toString("utf-8");
385
+ for (const line of chunk.split("\n")) {
386
+ const trimmed = line.trim();
387
+ if (!trimmed) continue;
388
+ try {
389
+ const event = JSON.parse(trimmed);
390
+ this.emit(event);
391
+ } catch {
392
+ logger4.debug("Skipping malformed event line", { line: trimmed });
393
+ }
394
+ }
395
+ } finally {
396
+ fs2.closeSync(fd);
397
+ }
398
+ } catch (err) {
399
+ logger4.debug("Error reading events file", { error: err.message });
400
+ }
401
+ }
402
+ readAll() {
403
+ if (!fs2.existsSync(this.eventsFile)) return [];
404
+ try {
405
+ const content = fs2.readFileSync(this.eventsFile, "utf-8").trim();
406
+ if (!content) return [];
407
+ return content.split("\n").reduce((acc, line) => {
408
+ const trimmed = line.trim();
409
+ if (!trimmed) return acc;
410
+ try {
411
+ acc.push(JSON.parse(trimmed));
412
+ } catch {
413
+ }
414
+ return acc;
415
+ }, []);
416
+ } catch {
417
+ return [];
418
+ }
419
+ }
420
+ emit(event) {
421
+ for (const listener of this.listeners) {
422
+ try {
423
+ listener(event);
424
+ } catch (err) {
425
+ logger4.warn("Event listener error", { error: err.message });
426
+ }
427
+ }
428
+ }
429
+ getCurrentSize() {
430
+ try {
431
+ return fs2.statSync(this.eventsFile).size;
432
+ } catch {
433
+ return 0;
434
+ }
435
+ }
436
+ };
437
+
438
+ // src/hooks/HookInjector.ts
439
+ import fs3 from "fs";
440
+ import path2 from "path";
441
+ var logger5 = logger.child("HookInjector");
442
+ var HOOKS_DIR = ".claude-plan/.hooks";
443
+ var EVENTS_FILE_NAME = ".hook-events.jsonl";
444
+ var MANIFEST_FILE_NAME = ".artifact-manifest.jsonl";
445
+ var CONTEXT_FILE_NAME = ".hook-context.json";
446
+ var HookInjector = class {
447
+ inject(ctx) {
448
+ this.writeHookScripts(ctx);
449
+ this.writeContextFile(ctx);
450
+ this.writeSettingsLocal(ctx);
451
+ this.initEventsFile(ctx);
452
+ logger5.info("Hooks injected", {
453
+ workDir: ctx.workDir,
454
+ issueIid: ctx.issueIid,
455
+ phase: ctx.phaseName,
456
+ artifacts: ctx.expectedArtifacts
457
+ });
458
+ }
459
+ /**
460
+ * 阶段切换时更新 hooks 配置(重写脚本 + settings.local.json)。
461
+ * 保留 events/manifest 文件(不截断),仅更新脚本和配置。
462
+ */
463
+ updateForPhase(ctx) {
464
+ this.writeHookScripts(ctx);
465
+ this.writeContextFile(ctx);
466
+ this.writeSettingsLocal(ctx);
467
+ logger5.info("Hooks updated for phase", {
468
+ workDir: ctx.workDir,
469
+ issueIid: ctx.issueIid,
470
+ phase: ctx.phaseName
471
+ });
472
+ }
473
+ readManifest(workDir) {
474
+ const manifestPath = path2.join(workDir, ".claude-plan", MANIFEST_FILE_NAME);
475
+ return readJsonl(manifestPath);
476
+ }
477
+ readEvents(workDir) {
478
+ const eventsPath = path2.join(workDir, ".claude-plan", EVENTS_FILE_NAME);
479
+ return readJsonl(eventsPath);
480
+ }
481
+ getEventsFilePath(workDir) {
482
+ return path2.join(workDir, ".claude-plan", EVENTS_FILE_NAME);
483
+ }
484
+ getManifestFilePath(workDir) {
485
+ return path2.join(workDir, ".claude-plan", MANIFEST_FILE_NAME);
486
+ }
487
+ cleanup(workDir) {
488
+ const hooksDir = path2.join(workDir, HOOKS_DIR);
489
+ try {
490
+ if (fs3.existsSync(hooksDir)) {
491
+ fs3.rmSync(hooksDir, { recursive: true });
492
+ }
493
+ } catch (err) {
494
+ logger5.warn("Failed to cleanup hooks", { error: err.message });
495
+ }
496
+ }
497
+ // ---------------------------------------------------------------------------
498
+ // Private
499
+ // ---------------------------------------------------------------------------
500
+ writeHookScripts(ctx) {
501
+ const hooksDir = path2.join(ctx.workDir, HOOKS_DIR);
502
+ fs3.mkdirSync(hooksDir, { recursive: true });
503
+ const eventsFile = path2.join(ctx.workDir, ".claude-plan", EVENTS_FILE_NAME);
504
+ const manifestFile = path2.join(ctx.workDir, ".claude-plan", MANIFEST_FILE_NAME);
505
+ const contextFile = path2.join(ctx.workDir, ".claude-plan", CONTEXT_FILE_NAME);
506
+ const expected = ctx.expectedArtifacts.join(",");
507
+ const phaseExpected = (ctx.phaseExpectedArtifacts ?? ctx.expectedArtifacts).join(",");
508
+ const scripts = [
509
+ { name: "session-start.sh", content: buildSessionStartScript(eventsFile) },
510
+ { name: "compact-restore.sh", content: buildCompactRestoreScript(eventsFile, contextFile) },
511
+ { name: "post-tool-use.sh", content: buildPostToolUseScript(eventsFile, manifestFile, expected) },
512
+ { name: "post-artifact.sh", content: buildPostArtifactScript(manifestFile, expected) },
513
+ { name: "exit-plan-mode.sh", content: buildExitPlanModeScript(eventsFile, ctx.planDir, ctx.phaseExpectedArtifacts?.[0] ?? ctx.expectedArtifacts[0]) },
514
+ { name: "permission.sh", content: buildPermissionScript(eventsFile) },
515
+ { name: "protect-files.sh", content: buildProtectFilesScript(eventsFile, ctx.phaseName, ctx.planDir) },
516
+ { name: "stop.sh", content: buildStopScript(eventsFile, ctx.planDir, phaseExpected) },
517
+ { name: "ask-user-hook.sh", content: buildAskUserHookScript(eventsFile) },
518
+ { name: "notification.sh", content: buildNotificationScript(eventsFile) }
519
+ ];
520
+ for (const { name, content } of scripts) {
521
+ const scriptPath = path2.join(hooksDir, name);
522
+ fs3.writeFileSync(scriptPath, content, { mode: 493 });
523
+ }
524
+ }
525
+ writeContextFile(ctx) {
526
+ const contextPath = path2.join(ctx.workDir, ".claude-plan", CONTEXT_FILE_NAME);
527
+ const context = {
528
+ issueIid: ctx.issueIid,
529
+ issueTitle: ctx.issueTitle ?? "",
530
+ issueDescription: ctx.issueDescription ?? "",
531
+ phaseName: ctx.phaseName ?? "",
532
+ expectedArtifacts: ctx.expectedArtifacts,
533
+ planDir: ctx.planDir
534
+ };
535
+ fs3.writeFileSync(contextPath, JSON.stringify(context, null, 2), "utf-8");
536
+ }
537
+ writeSettingsLocal(ctx) {
538
+ const claudeDir = path2.join(ctx.workDir, ".claude");
539
+ fs3.mkdirSync(claudeDir, { recursive: true });
540
+ const settingsPath = path2.join(claudeDir, "settings.local.json");
541
+ let existing = {};
542
+ if (fs3.existsSync(settingsPath)) {
543
+ try {
544
+ existing = JSON.parse(fs3.readFileSync(settingsPath, "utf-8"));
545
+ } catch {
546
+ logger5.warn("Failed to parse existing settings.local.json, overwriting");
547
+ }
548
+ }
549
+ const hooksDir = path2.join(ctx.workDir, HOOKS_DIR);
550
+ const hooks = buildHooksConfig(hooksDir, ctx);
551
+ const merged = { ...existing, hooks };
552
+ fs3.writeFileSync(settingsPath, JSON.stringify(merged, null, 2), "utf-8");
553
+ }
554
+ initEventsFile(ctx) {
555
+ const eventsPath = path2.join(ctx.workDir, ".claude-plan", EVENTS_FILE_NAME);
556
+ const manifestPath = path2.join(ctx.workDir, ".claude-plan", MANIFEST_FILE_NAME);
557
+ fs3.writeFileSync(eventsPath, "", "utf-8");
558
+ fs3.writeFileSync(manifestPath, "", "utf-8");
559
+ }
560
+ };
561
+ function buildHooksConfig(hooksDir, ctx) {
562
+ const isPlanPhase = ctx.phaseName === "plan";
563
+ const artifactIfPatterns = buildArtifactIfPatterns(ctx.expectedArtifacts);
564
+ const config = {
565
+ SessionStart: [
566
+ {
567
+ hooks: [{
568
+ type: "command",
569
+ command: path2.join(hooksDir, "session-start.sh"),
570
+ timeout: 5
571
+ }]
572
+ },
573
+ {
574
+ matcher: "compact",
575
+ hooks: [{
576
+ type: "command",
577
+ command: path2.join(hooksDir, "compact-restore.sh"),
578
+ timeout: 5
579
+ }]
580
+ }
581
+ ],
582
+ PreToolUse: [
583
+ {
584
+ matcher: "AskUserQuestion",
585
+ hooks: [{
586
+ type: "command",
587
+ command: path2.join(hooksDir, "ask-user-hook.sh"),
588
+ timeout: 5
589
+ }]
590
+ },
591
+ {
592
+ matcher: "Edit|Write",
593
+ hooks: [{
594
+ type: "command",
595
+ command: path2.join(hooksDir, "protect-files.sh"),
596
+ timeout: 5,
597
+ ...buildProtectIfClause(ctx.phaseName)
598
+ }]
599
+ }
600
+ ],
601
+ PostToolUse: buildPostToolUseConfig(hooksDir, artifactIfPatterns),
602
+ PermissionRequest: buildPermissionRequestConfig(hooksDir, isPlanPhase),
603
+ Notification: [{
604
+ hooks: [{
605
+ type: "command",
606
+ command: path2.join(hooksDir, "notification.sh"),
607
+ timeout: 5
608
+ }]
609
+ }],
610
+ Stop: [{
611
+ hooks: [{
612
+ type: "command",
613
+ command: path2.join(hooksDir, "stop.sh"),
614
+ timeout: 15
615
+ }]
616
+ }]
617
+ };
618
+ return config;
619
+ }
620
+ function buildPermissionRequestConfig(hooksDir, isPlanPhase) {
621
+ const groups = [];
622
+ if (isPlanPhase) {
623
+ groups.push({
624
+ matcher: "ExitPlanMode",
625
+ hooks: [{
626
+ type: "command",
627
+ command: path2.join(hooksDir, "exit-plan-mode.sh"),
628
+ timeout: 5
629
+ }]
630
+ });
631
+ }
632
+ groups.push({
633
+ matcher: "Bash|Edit|Write|Read|Glob|Grep|WebFetch|WebSearch|mcp__.*",
634
+ hooks: [{
635
+ type: "command",
636
+ command: path2.join(hooksDir, "permission.sh"),
637
+ timeout: 5
638
+ }]
639
+ });
640
+ return groups;
641
+ }
642
+ function buildPostToolUseConfig(hooksDir, artifactIfPatterns) {
643
+ const groups = [];
644
+ if (artifactIfPatterns) {
645
+ groups.push({
646
+ matcher: "Write|Edit",
647
+ hooks: [{
648
+ type: "command",
649
+ command: path2.join(hooksDir, "post-artifact.sh"),
650
+ timeout: 10,
651
+ if: artifactIfPatterns
652
+ }]
653
+ });
654
+ }
655
+ groups.push({
656
+ matcher: "Write|Edit",
657
+ hooks: [{
658
+ type: "command",
659
+ command: path2.join(hooksDir, "post-tool-use.sh"),
660
+ timeout: 10
661
+ }]
662
+ });
663
+ return groups;
664
+ }
665
+ function buildArtifactIfPatterns(artifacts) {
666
+ if (artifacts.length === 0) return void 0;
667
+ return artifacts.flatMap((f) => [`Write(*${f})`, `Edit(*${f})`]).join("|");
668
+ }
669
+ function buildProtectIfClause(_phaseName) {
670
+ const alwaysProtected = [".env", ".env.*", "package-lock.json", "pnpm-lock.yaml"];
671
+ const ifValue = alwaysProtected.flatMap((f) => [`Edit(*${f})`, `Write(*${f})`]).join("|");
672
+ return { if: ifValue };
673
+ }
674
+ function buildSessionStartScript(eventsFile) {
675
+ return `#!/bin/bash
676
+ set -euo pipefail
677
+ INPUT=$(cat)
678
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
679
+ printf '{"ts":"%s","event":"session_start","session_id":"%s"}\\n' \\
680
+ "$(date -u +%FT%TZ)" "$SESSION_ID" >> ${quote(eventsFile)}
681
+ exit 0
682
+ `;
683
+ }
684
+ function buildCompactRestoreScript(eventsFile, contextFile) {
685
+ return `#!/bin/bash
686
+ set -euo pipefail
687
+
688
+ CONTEXT_FILE=${quote(contextFile)}
689
+ if [ ! -f "$CONTEXT_FILE" ]; then
690
+ exit 0
691
+ fi
692
+
693
+ ISSUE_IID=$(jq -r '.issueIid // empty' < "$CONTEXT_FILE")
694
+ ISSUE_TITLE=$(jq -r '.issueTitle // empty' < "$CONTEXT_FILE")
695
+ ISSUE_DESC=$(jq -r '.issueDescription // empty' < "$CONTEXT_FILE")
696
+ PHASE=$(jq -r '.phaseName // empty' < "$CONTEXT_FILE")
697
+ PLAN_DIR=$(jq -r '.planDir // empty' < "$CONTEXT_FILE")
698
+ ARTIFACTS=$(jq -r '.expectedArtifacts | join(", ") // empty' < "$CONTEXT_FILE")
699
+
700
+ READY=""
701
+ MISSING=""
702
+ for f in $(jq -r '.expectedArtifacts[]' < "$CONTEXT_FILE" 2>/dev/null); do
703
+ FPATH="$PLAN_DIR/$f"
704
+ if [ -f "$FPATH" ] && [ "$(wc -c < "$FPATH")" -ge 50 ]; then
705
+ READY="$READY $f"
706
+ else
707
+ MISSING="$MISSING $f"
708
+ fi
709
+ done
710
+ READY=$(echo "$READY" | xargs)
711
+ MISSING=$(echo "$MISSING" | xargs)
712
+
713
+ printf '{"ts":"%s","event":"compact_restore"}\\n' "$(date -u +%FT%TZ)" >> ${quote(eventsFile)}
714
+
715
+ cat <<CONTEXT
716
+ [\u4E0A\u4E0B\u6587\u6062\u590D \u2014 compaction \u540E\u81EA\u52A8\u6CE8\u5165]
717
+ Issue #$ISSUE_IID: $ISSUE_TITLE
718
+ \u5F53\u524D\u9636\u6BB5: $PHASE
719
+ \u9884\u671F\u4EA7\u7269: $ARTIFACTS
720
+ \u5DF2\u5C31\u7EEA: \${READY:-\u65E0}
721
+ \u672A\u5B8C\u6210: \${MISSING:-\u65E0}
722
+
723
+ \u9700\u6C42\u63CF\u8FF0:
724
+ $ISSUE_DESC
725
+ CONTEXT
726
+ exit 0
727
+ `;
728
+ }
729
+ function buildPostToolUseScript(eventsFile, manifestFile, expected) {
730
+ return `#!/bin/bash
731
+ set -euo pipefail
732
+ INPUT=$(cat)
733
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
734
+ [ -z "$FILE_PATH" ] && exit 0
735
+
736
+ EXPECTED=${quote(expected)}
737
+ BASENAME=$(basename "$FILE_PATH")
738
+
739
+ if echo "$EXPECTED" | tr ',' '\\n' | grep -qx "$BASENAME"; then
740
+ BYTES=$(wc -c < "$FILE_PATH" 2>/dev/null || echo 0)
741
+ printf '{"ts":"%s","event":"artifact_write","file":"%s","path":"%s","bytes":%s}\\n' \\
742
+ "$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" "$BYTES" >> ${quote(manifestFile)}
743
+ fi
744
+
745
+ printf '{"ts":"%s","event":"artifact_write","file":"%s","path":"%s","bytes":0}\\n' \\
746
+ "$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" >> ${quote(eventsFile)}
747
+ exit 0
748
+ `;
749
+ }
750
+ function buildPostArtifactScript(manifestFile, expected) {
751
+ return `#!/bin/bash
752
+ set -euo pipefail
753
+ INPUT=$(cat)
754
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
755
+ [ -z "$FILE_PATH" ] && exit 0
756
+
757
+ EXPECTED=${quote(expected)}
758
+ BASENAME=$(basename "$FILE_PATH")
759
+
760
+ if echo "$EXPECTED" | tr ',' '\\n' | grep -qx "$BASENAME"; then
761
+ BYTES=$(wc -c < "$FILE_PATH" 2>/dev/null || echo 0)
762
+ printf '{"ts":"%s","event":"write","file":"%s","path":"%s","bytes":%s}\\n' \\
763
+ "$(date -u +%FT%TZ)" "$BASENAME" "$FILE_PATH" "$BYTES" >> ${quote(manifestFile)}
764
+ fi
765
+ exit 0
766
+ `;
767
+ }
768
+ function buildExitPlanModeScript(eventsFile, planDir, planArtifact) {
769
+ return `#!/bin/bash
770
+ set -euo pipefail
771
+ INPUT=$(cat)
772
+
773
+ PLAN_DIR=${quote(planDir)}
774
+ PLAN_ARTIFACT=${quote(planArtifact ?? "01-plan.md")}
775
+
776
+ PLAN_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.plan // empty')
777
+ if [ -n "$PLAN_CONTENT" ]; then
778
+ mkdir -p "$PLAN_DIR"
779
+ printf '%s' "$PLAN_CONTENT" > "$PLAN_DIR/$PLAN_ARTIFACT"
780
+ BYTES=$(wc -c < "$PLAN_DIR/$PLAN_ARTIFACT")
781
+ printf '{"ts":"%s","event":"plan_captured","file":"%s","path":"%s/%s","bytes":%s}\\n' \\
782
+ "$(date -u +%FT%TZ)" "$PLAN_ARTIFACT" "$PLAN_DIR" "$PLAN_ARTIFACT" "$BYTES" >> ${quote(eventsFile)}
783
+ fi
784
+
785
+ printf '{"ts":"%s","event":"exit_plan_mode"}\\n' "$(date -u +%FT%TZ)" >> ${quote(eventsFile)}
786
+
787
+ echo '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
788
+ exit 0
789
+ `;
790
+ }
791
+ function buildPermissionScript(eventsFile) {
792
+ return `#!/bin/bash
793
+ set -euo pipefail
794
+ INPUT=$(cat)
795
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
796
+ printf '{"ts":"%s","event":"permission_request","tool":"%s"}\\n' \\
797
+ "$(date -u +%FT%TZ)" "$TOOL" >> ${quote(eventsFile)}
798
+ echo '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
799
+ exit 0
800
+ `;
801
+ }
802
+ function buildProtectFilesScript(eventsFile, _phaseName, _planDir) {
803
+ return `#!/bin/bash
804
+ set -euo pipefail
805
+ INPUT=$(cat)
806
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
807
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
808
+ [ -z "$FILE_PATH" ] && exit 0
809
+
810
+ BASENAME=$(basename "$FILE_PATH")
811
+
812
+ blocked_reason() {
813
+ printf '{"ts":"%s","event":"protect_blocked","tool":"%s","file":"%s"}\\n' \\
814
+ "$(date -u +%FT%TZ)" "$TOOL" "$BASENAME" >> ${quote(eventsFile)}
815
+ echo "$1" >&2
816
+ exit 2
817
+ }
818
+
819
+ case "$BASENAME" in
820
+ .env|.env.*)
821
+ 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"
822
+ ;;
823
+ package-lock.json|pnpm-lock.yaml)
824
+ 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"
825
+ ;;
826
+ esac
827
+
828
+ exit 0
829
+ `;
830
+ }
831
+ function buildStopScript(eventsFile, planDir, phaseExpected) {
832
+ return `#!/bin/bash
833
+ set -euo pipefail
834
+ INPUT=$(cat)
835
+ STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
836
+
837
+ PLAN_DIR=${quote(planDir)}
838
+ MIN_BYTES=50
839
+ PHASE_EXPECTED=${quote(phaseExpected)}
840
+
841
+ MISSING=""
842
+ READY=""
843
+ for f in $(echo "$PHASE_EXPECTED" | tr ',' ' '); do
844
+ [ -z "$f" ] && continue
845
+ FPATH="$PLAN_DIR/$f"
846
+ if [ -f "$FPATH" ] && [ "$(wc -c < "$FPATH")" -ge "$MIN_BYTES" ]; then
847
+ BYTES=$(wc -c < "$FPATH")
848
+ READY="$READY $f(\${BYTES} bytes)"
849
+ else
850
+ MISSING="$MISSING $f"
851
+ fi
852
+ done
853
+
854
+ MISSING=$(echo "$MISSING" | xargs)
855
+ READY=$(echo "$READY" | xargs)
856
+
857
+ if [ -n "$MISSING" ] && [ "$STOP_ACTIVE" != "true" ]; then
858
+ printf '{"ts":"%s","event":"stop","blocked":true,"missing":"%s"}\\n' \\
859
+ "$(date -u +%FT%TZ)" "$MISSING" >> ${quote(eventsFile)}
860
+
861
+ 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}"
862
+
863
+ printf '{"decision":"block","reason":"%s"}' "$REASON"
864
+ exit 0
865
+ fi
866
+
867
+ printf '{"ts":"%s","event":"stop","blocked":false,"missing":"%s"}\\n' \\
868
+ "$(date -u +%FT%TZ)" "\${MISSING:-none}" >> ${quote(eventsFile)}
869
+ exit 0
870
+ `;
871
+ }
872
+ function buildAskUserHookScript(eventsFile) {
873
+ return `#!/bin/bash
874
+ set -euo pipefail
875
+ INPUT=$(cat)
876
+ QUESTION=$(echo "$INPUT" | jq -r '.tool_input.question // empty')
877
+ OPTIONS=$(echo "$INPUT" | jq -c '[.tool_input.options[]? | {index: .index, label: .label}] // []' 2>/dev/null || echo '[]')
878
+ [ -z "$QUESTION" ] && exit 0
879
+ printf '{"ts":"%s","event":"ask_user_question","question":"%s","options":%s}\\n' \\
880
+ "$(date -u +%FT%TZ)" "$(echo "$QUESTION" | head -c 500 | tr '"' "'")" "$OPTIONS" >> ${quote(eventsFile)}
881
+ exit 0
882
+ `;
883
+ }
884
+ function buildNotificationScript(eventsFile) {
885
+ return `#!/bin/bash
886
+ set -euo pipefail
887
+ INPUT=$(cat)
888
+ NTYPE=$(echo "$INPUT" | jq -r '.notification_type // empty')
889
+ MSG=$(echo "$INPUT" | jq -r '.message // empty' | head -c 200 | tr '"' "'")
890
+ [ -z "$NTYPE" ] && exit 0
891
+ printf '{"ts":"%s","event":"notification","notification_type":"%s","message":"%s"}\\n' \\
892
+ "$(date -u +%FT%TZ)" "$NTYPE" "$MSG" >> ${quote(eventsFile)}
893
+ exit 0
894
+ `;
895
+ }
896
+ function quote(s) {
897
+ return `"${s.replace(/"/g, '\\"')}"`;
898
+ }
899
+ function readJsonl(filePath) {
900
+ if (!fs3.existsSync(filePath)) return [];
901
+ try {
902
+ const content = fs3.readFileSync(filePath, "utf-8").trim();
903
+ if (!content) return [];
904
+ return content.split("\n").reduce((acc, line) => {
905
+ const trimmed = line.trim();
906
+ if (!trimmed) return acc;
907
+ try {
908
+ acc.push(JSON.parse(trimmed));
909
+ } catch {
910
+ logger5.debug("Skipping malformed JSONL line", { line: trimmed });
911
+ }
912
+ return acc;
913
+ }, []);
914
+ } catch {
915
+ return [];
916
+ }
917
+ }
918
+
203
919
  // src/ai-runner/PtyRunner.ts
204
- var logger3 = logger.child("PtyRunner");
920
+ var logger6 = logger.child("PtyRunner");
205
921
  var ANSI_RE = /\x1b\[[?><=]*[0-9;]*[a-zA-Z~]|\x1b\][^\x07]*\x07|\x1b\(B/g;
206
922
  function stripAnsi(str) {
207
923
  return str.replace(ANSI_RE, "");
@@ -340,6 +1056,34 @@ function isTuiNoise(line) {
340
1056
  if (CLAUDE_BANNER_INFO_RE.test(t)) return true;
341
1057
  return false;
342
1058
  }
1059
+ var InputWaitController = class {
1060
+ constructor(totalBudgetMs) {
1061
+ this.totalBudgetMs = totalBudgetMs;
1062
+ this.wallClockSegmentStart = Date.now();
1063
+ }
1064
+ _waiting = false;
1065
+ wallClockUsedMs = 0;
1066
+ wallClockSegmentStart;
1067
+ get waiting() {
1068
+ return this._waiting;
1069
+ }
1070
+ pause() {
1071
+ if (this._waiting) return;
1072
+ this._waiting = true;
1073
+ this.wallClockUsedMs += Date.now() - this.wallClockSegmentStart;
1074
+ }
1075
+ resume() {
1076
+ if (!this._waiting) return;
1077
+ this._waiting = false;
1078
+ this.wallClockSegmentStart = Date.now();
1079
+ }
1080
+ get remainingMs() {
1081
+ return Math.max(this.totalBudgetMs - this.wallClockUsedMs, 6e4);
1082
+ }
1083
+ get usedMs() {
1084
+ return this.wallClockUsedMs;
1085
+ }
1086
+ };
343
1087
  var PtyRunner = class {
344
1088
  constructor(nvmNodeVersion, terminalManager, defaultAgentMode, phaseAgentMap, globalModel, idleDetectMs = 3e4) {
345
1089
  this.nvmNodeVersion = nvmNodeVersion;
@@ -357,32 +1101,32 @@ var PtyRunner = class {
357
1101
  // ---- AIRunner interface ---------------------------------------------------
358
1102
  async run(options) {
359
1103
  if (isShuttingDown()) {
360
- logger3.warn("PtyRunner skipped \u2014 service is shutting down");
1104
+ logger6.warn("PtyRunner skipped \u2014 service is shutting down");
361
1105
  return { success: false, output: "Service shutting down", exitCode: null };
362
1106
  }
363
1107
  const { prompt, workDir, timeoutMs, onStreamEvent, phaseName } = options;
364
1108
  const agentMode = this.resolveAgentForPhase(phaseName);
365
1109
  const startMode = options.mode;
366
1110
  const continueSession = options.continueSession ?? false;
367
- logger3.info("PtyRunner.run()", { workDir, timeoutMs, phaseName, agentMode, continueSession });
1111
+ logger6.info("PtyRunner.run()", { workDir, timeoutMs, phaseName, agentMode, continueSession });
368
1112
  const { sessionId, isNew } = this.ensureSession(workDir, agentMode, startMode);
369
1113
  if (isNew) {
370
- logger3.info("Waiting for AI agent prompt (new session)", { sessionId, phaseName });
1114
+ logger6.info("Waiting for AI agent prompt (new session)", { sessionId, phaseName });
371
1115
  await this.waitForPrompt(sessionId, 3e5);
372
1116
  } else if (continueSession) {
373
- logger3.info("Waiting for AI agent prompt (continue-session)", { sessionId, phaseName });
1117
+ logger6.info("Waiting for AI agent prompt (continue-session)", { sessionId, phaseName });
374
1118
  await this.waitForPrompt(sessionId, 3e4);
375
1119
  } else {
376
- logger3.info("Waiting for AI agent prompt (reused session)", { sessionId, phaseName });
1120
+ logger6.info("Waiting for AI agent prompt (reused session)", { sessionId, phaseName });
377
1121
  await this.waitForPrompt(sessionId, 1e4);
378
1122
  }
379
1123
  if (startMode === "plan" && this.shouldUseNativePlan(agentMode)) {
380
1124
  return this.runNativePlanMode(sessionId, isNew, options, agentMode, workDir);
381
1125
  }
382
1126
  if (continueSession && !isNew) {
383
- logger3.info("Continue-session mode: attaching detectCompletion (no /clear)", { sessionId });
1127
+ logger6.info("Continue-session mode: attaching detectCompletion (no /clear)", { sessionId });
384
1128
  const result2 = await this.detectCompletion(sessionId, options, onStreamEvent);
385
- logger3.info("PtyRunner continue-session completed", {
1129
+ logger6.info("PtyRunner continue-session completed", {
386
1130
  workDir,
387
1131
  agentMode,
388
1132
  phaseName,
@@ -398,7 +1142,7 @@ var PtyRunner = class {
398
1142
  const instruction = `Please read and follow all instructions in ${promptFile}`;
399
1143
  await this.writeCommand(sessionId, instruction, agentMode);
400
1144
  const result = await this.detectCompletion(sessionId, options, onStreamEvent);
401
- logger3.info("PtyRunner phase completed", {
1145
+ logger6.info("PtyRunner phase completed", {
402
1146
  workDir,
403
1147
  agentMode,
404
1148
  phaseName,
@@ -413,7 +1157,7 @@ var PtyRunner = class {
413
1157
  this.terminalManager.destroy(info.sessionId);
414
1158
  }
415
1159
  this.sessions.clear();
416
- logger3.info("PtyRunner: all managed sessions destroyed");
1160
+ logger6.info("PtyRunner: all managed sessions destroyed");
417
1161
  }
418
1162
  killByWorkDir(targetWorkDir) {
419
1163
  const info = this.sessions.get(targetWorkDir);
@@ -432,7 +1176,7 @@ var PtyRunner = class {
432
1176
  return false;
433
1177
  }
434
1178
  this.terminalManager.write(info.sessionId, "");
435
- logger3.info("Interrupted PTY session for retry", {
1179
+ logger6.info("Interrupted PTY session for retry", {
436
1180
  workDir: targetWorkDir,
437
1181
  sessionId: info.sessionId
438
1182
  });
@@ -489,7 +1233,7 @@ var PtyRunner = class {
489
1233
  ensureSession(workDir, agentMode, startMode) {
490
1234
  const existing = this.sessions.get(workDir);
491
1235
  if (existing && existing.agentMode !== agentMode) {
492
- logger3.info("Agent switched, destroying old PTY session", {
1236
+ logger6.info("Agent switched, destroying old PTY session", {
493
1237
  workDir,
494
1238
  oldAgent: existing.agentMode,
495
1239
  newAgent: agentMode,
@@ -500,7 +1244,7 @@ var PtyRunner = class {
500
1244
  }
501
1245
  if (existing && existing.agentMode === agentMode) {
502
1246
  if (this.terminalManager.get(existing.sessionId)) {
503
- logger3.info("Reusing existing PTY session (same agent)", {
1247
+ logger6.info("Reusing existing PTY session (same agent)", {
504
1248
  workDir,
505
1249
  agentMode,
506
1250
  sessionId: existing.sessionId
@@ -511,7 +1255,7 @@ var PtyRunner = class {
511
1255
  }
512
1256
  const orphan = this.terminalManager.findByWorkDir(workDir);
513
1257
  if (orphan) {
514
- logger3.info("Destroying orphaned PTY session on workDir", {
1258
+ logger6.info("Destroying orphaned PTY session on workDir", {
515
1259
  workDir,
516
1260
  sessionId: orphan.id
517
1261
  });
@@ -535,7 +1279,7 @@ var PtyRunner = class {
535
1279
  currentMode: profile.defaultModeName ?? "bypass",
536
1280
  startedWithMode: startMode
537
1281
  });
538
- logger3.info("Created new PTY session", {
1282
+ logger6.info("Created new PTY session", {
539
1283
  workDir,
540
1284
  agentMode,
541
1285
  binary,
@@ -567,7 +1311,7 @@ var PtyRunner = class {
567
1311
  if (stabilityTimer) clearTimeout(stabilityTimer);
568
1312
  if (silenceTimer) clearTimeout(silenceTimer);
569
1313
  subscription.dispose();
570
- logger3.warn("Timed out waiting for AI agent prompt", { sessionId, timeoutMs });
1314
+ logger6.warn("Timed out waiting for AI agent prompt", { sessionId, timeoutMs });
571
1315
  resolve();
572
1316
  }, timeoutMs);
573
1317
  const done = (reason) => {
@@ -575,7 +1319,7 @@ var PtyRunner = class {
575
1319
  if (stabilityTimer) clearTimeout(stabilityTimer);
576
1320
  if (silenceTimer) clearTimeout(silenceTimer);
577
1321
  subscription.dispose();
578
- logger3.info("AI agent prompt detected", { sessionId, reason });
1322
+ logger6.info("AI agent prompt detected", { sessionId, reason });
579
1323
  resolve();
580
1324
  };
581
1325
  const resetSilenceTimer = () => {
@@ -583,7 +1327,7 @@ var PtyRunner = class {
583
1327
  if (silenceTimer) clearTimeout(silenceTimer);
584
1328
  silenceTimer = setTimeout(() => {
585
1329
  if (promptSeen) return;
586
- logger3.info("Banner shown and PTY silent \u2014 treating agent as ready", { sessionId });
1330
+ logger6.info("Banner shown and PTY silent \u2014 treating agent as ready", { sessionId });
587
1331
  done("silence-after-banner");
588
1332
  }, SILENCE_READY_MS);
589
1333
  };
@@ -597,11 +1341,11 @@ var PtyRunner = class {
597
1341
  resetSilenceTimer();
598
1342
  if (!trustDialogHandled && TRUST_DIALOG_RE.test(stripped)) {
599
1343
  trustDialogHandled = true;
600
- logger3.info("Trust dialog detected, auto-confirming", { sessionId });
1344
+ logger6.info("Trust dialog detected, auto-confirming", { sessionId });
601
1345
  setTimeout(() => this.terminalManager.write(sessionId, "\r"), 500);
602
1346
  }
603
1347
  if (PERMISSION_DIALOG_RE.test(stripped)) {
604
- logger3.info("Permission dialog detected in waitForPrompt, auto-confirming", { sessionId });
1348
+ logger6.info("Permission dialog detected in waitForPrompt, auto-confirming", { sessionId });
605
1349
  setTimeout(() => this.terminalManager.write(sessionId, "\r"), 500);
606
1350
  }
607
1351
  if (isIdlePrompt(stripped)) {
@@ -636,7 +1380,7 @@ var PtyRunner = class {
636
1380
  if (!session) return;
637
1381
  const targetMode = wantPlan ? profile.planModeName : profile.defaultModeName ?? "bypass";
638
1382
  if (session.currentMode === targetMode) {
639
- logger3.info("PTY already in target mode", { sessionId, targetMode });
1383
+ logger6.info("PTY already in target mode", { sessionId, targetMode });
640
1384
  return;
641
1385
  }
642
1386
  const MAX_ATTEMPTS = 5;
@@ -648,7 +1392,7 @@ var PtyRunner = class {
648
1392
  if (detected) {
649
1393
  session.currentMode = detected;
650
1394
  if (detected === targetMode) {
651
- logger3.info("PTY mode switched", {
1395
+ logger6.info("PTY mode switched", {
652
1396
  sessionId,
653
1397
  agentMode,
654
1398
  targetMode,
@@ -658,7 +1402,7 @@ var PtyRunner = class {
658
1402
  }
659
1403
  }
660
1404
  }
661
- logger3.warn("Failed to switch PTY mode after max attempts", {
1405
+ logger6.warn("Failed to switch PTY mode after max attempts", {
662
1406
  sessionId,
663
1407
  agentMode,
664
1408
  targetMode,
@@ -712,9 +1456,9 @@ var PtyRunner = class {
712
1456
  const issueIid = extractIidFromPath(workDir);
713
1457
  const contentHint = issueIid ? PlanFileResolver.buildContentHint(issueIid) : void 0;
714
1458
  if (continueSession) {
715
- logger3.info("Native plan mode: continue-session (no /clear, no prompt)", { sessionId });
1459
+ logger6.info("Native plan mode: continue-session (no /clear, no prompt)", { sessionId });
716
1460
  } else {
717
- logger3.info("Native plan mode: switching to plan", { sessionId, agentMode });
1461
+ logger6.info("Native plan mode: switching to plan", { sessionId, agentMode });
718
1462
  await this.ensurePlanMode(sessionId, agentMode, true, workDir);
719
1463
  if (!isNew) {
720
1464
  await this.writeCommand(sessionId, "/clear", agentMode);
@@ -734,47 +1478,62 @@ var PtyRunner = class {
734
1478
  artifactCheck: planArtifactCheck
735
1479
  }, options.onStreamEvent, continueSession);
736
1480
  if (planResult.timedOut) {
737
- logger3.warn("Native plan mode: plan phase timed out", {
1481
+ logger6.warn("Native plan mode: plan phase timed out", {
738
1482
  sessionId,
739
1483
  wasActive: planResult.wasActiveAtTimeout
740
1484
  });
741
1485
  return this.buildRunResult(planResult, sessionId);
742
1486
  }
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
1487
  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
- });
1488
+ const MIN_PLAN_BYTES = 50;
1489
+ const hookHandled = artifactPaths.length > 0 && artifactPaths.every((p) => {
1490
+ try {
1491
+ return fs4.existsSync(p) && fs4.statSync(p).size >= MIN_PLAN_BYTES;
1492
+ } catch {
1493
+ return false;
768
1494
  }
769
- } else {
770
- logger3.warn("Native plan mode: no artifactPaths specified, plan file not copied", {
1495
+ });
1496
+ if (hookHandled) {
1497
+ logger6.info("Native plan mode: plan captured by ExitPlanMode hook", {
771
1498
  sessionId,
772
- resolvedFile: resolved.sourcePath
1499
+ artifactPaths
773
1500
  });
1501
+ } else {
1502
+ logger6.info("Native plan mode: resolving plan file from CLI storage (fallback)", { sessionId });
1503
+ const resolved = resolver.resolve(contentHint);
1504
+ if (!resolved) {
1505
+ logger6.error("Native plan mode: no plan file found in CLI storage", { sessionId, workDir });
1506
+ return {
1507
+ success: false,
1508
+ output: planResult.output,
1509
+ errorMessage: "Plan \u9636\u6BB5\u5B8C\u6210\u4F46\u672A\u5728 CLI \u8BA1\u5212\u76EE\u5F55\u4E2D\u627E\u5230\u8BA1\u5212\u6587\u4EF6",
1510
+ sessionId,
1511
+ exitCode: null
1512
+ };
1513
+ }
1514
+ if (artifactPaths.length > 0) {
1515
+ for (const targetPath of artifactPaths) {
1516
+ const targetDir = path3.dirname(targetPath);
1517
+ if (!fs4.existsSync(targetDir)) {
1518
+ fs4.mkdirSync(targetDir, { recursive: true });
1519
+ }
1520
+ fs4.writeFileSync(targetPath, resolved.content, "utf-8");
1521
+ logger6.info("Plan file copied to artifact path", {
1522
+ source: path3.basename(resolved.sourcePath),
1523
+ target: targetPath,
1524
+ size: resolved.content.length
1525
+ });
1526
+ }
1527
+ } else {
1528
+ logger6.warn("Native plan mode: no artifactPaths specified, plan file not copied", {
1529
+ sessionId,
1530
+ resolvedFile: resolved.sourcePath
1531
+ });
1532
+ }
774
1533
  }
775
- logger3.info("Native plan mode completed (deterministic copy)", {
1534
+ logger6.info("Native plan mode completed", {
776
1535
  sessionId,
777
- planSource: path2.basename(resolved.sourcePath),
1536
+ hookHandled,
778
1537
  artifactsCopied: artifactPaths.length
779
1538
  });
780
1539
  return this.buildRunResult(planResult, sessionId);
@@ -792,13 +1551,19 @@ var PtyRunner = class {
792
1551
  };
793
1552
  }
794
1553
  writePromptFile(workDir, prompt) {
795
- const dir = path2.join(workDir, ".claude-plan");
796
- if (!fs2.existsSync(dir)) {
797
- fs2.mkdirSync(dir, { recursive: true });
1554
+ const dir = path3.join(workDir, ".claude-plan");
1555
+ if (!fs4.existsSync(dir)) {
1556
+ fs4.mkdirSync(dir, { recursive: true });
1557
+ }
1558
+ const gitignorePath = path3.join(dir, ".gitignore");
1559
+ const entry = ".phase-prompt.md";
1560
+ if (!fs4.existsSync(gitignorePath) || !fs4.readFileSync(gitignorePath, "utf-8").includes(entry)) {
1561
+ fs4.appendFileSync(gitignorePath, `${entry}
1562
+ `, "utf-8");
798
1563
  }
799
1564
  const relPath = ".claude-plan/.phase-prompt.md";
800
- const absPath = path2.join(workDir, relPath);
801
- fs2.writeFileSync(absPath, prompt, "utf-8");
1565
+ const absPath = path3.join(workDir, relPath);
1566
+ fs4.writeFileSync(absPath, prompt, "utf-8");
802
1567
  return relPath;
803
1568
  }
804
1569
  // ---- Completion detection -------------------------------------------------
@@ -818,6 +1583,27 @@ var PtyRunner = class {
818
1583
  let pendingDialogParsed = null;
819
1584
  const idleTimeoutMs = options.idleTimeoutMs ?? 6e5;
820
1585
  const timeoutMs = options.timeoutMs;
1586
+ const inputWait = new InputWaitController(timeoutMs);
1587
+ const pauseTimersForInput = () => {
1588
+ if (inputWait.waiting) return;
1589
+ inputWait.pause();
1590
+ clearTimeout(wallTimer);
1591
+ logger6.info("Timers paused \u2014 waiting for user input", {
1592
+ sessionId,
1593
+ wallClockUsedMs: inputWait.usedMs
1594
+ });
1595
+ };
1596
+ const resumeTimersAfterInput = () => {
1597
+ if (!inputWait.waiting) return;
1598
+ inputWait.resume();
1599
+ lastOutputTime = Date.now();
1600
+ wallTimer = scheduleWallTimer(inputWait.remainingMs);
1601
+ logger6.info("Timers resumed after user input", {
1602
+ sessionId,
1603
+ remainingMs: inputWait.remainingMs,
1604
+ wallClockUsedMs: inputWait.usedMs
1605
+ });
1606
+ };
821
1607
  const GRACE_WINDOW_MS = options.timeoutGraceMs ?? 6e4;
822
1608
  const EXTENSION_MS = options.timeoutExtensionMs ?? 6e5;
823
1609
  const MAX_EXTENSIONS = options.timeoutMaxExtensions ?? 3;
@@ -830,11 +1616,12 @@ var PtyRunner = class {
830
1616
  };
831
1617
  const scheduleWallTimer = (delayMs) => {
832
1618
  return setTimeout(() => {
1619
+ if (inputWait.waiting) return;
833
1620
  const recentMs = Date.now() - lastOutputTime;
834
1621
  const isActive = hasSubstantiveOutput && recentMs < GRACE_WINDOW_MS;
835
1622
  if (isActive && extensions < MAX_EXTENSIONS) {
836
1623
  extensions++;
837
- logger3.info("Wall-clock timeout extended (agent still active)", {
1624
+ logger6.info("Wall-clock timeout extended (agent still active)", {
838
1625
  sessionId,
839
1626
  extensions,
840
1627
  maxExtensions: MAX_EXTENSIONS,
@@ -853,6 +1640,7 @@ var PtyRunner = class {
853
1640
  };
854
1641
  let wallTimer = scheduleWallTimer(timeoutMs);
855
1642
  const idleCheck = setInterval(() => {
1643
+ if (inputWait.waiting) return;
856
1644
  if (!hasSubstantiveOutput) return;
857
1645
  if (Date.now() - lastOutputTime >= idleTimeoutMs) {
858
1646
  finish({
@@ -863,6 +1651,52 @@ var PtyRunner = class {
863
1651
  });
864
1652
  }
865
1653
  }, 5e3);
1654
+ const dialogClassifier = new DialogClassifier();
1655
+ let hookWatcher;
1656
+ let hookSub;
1657
+ const hookInjector = new HookInjector();
1658
+ const eventsFilePath = hookInjector.getEventsFilePath(options.workDir);
1659
+ if (fs4.existsSync(eventsFilePath)) {
1660
+ hookWatcher = new HookEventWatcher(eventsFilePath);
1661
+ hookSub = hookWatcher.onEvent((event) => {
1662
+ dialogClassifier.ingestHookEvent(event);
1663
+ if (event.event === "ask_user_question" && options.onInputRequired && !dialogHandled && !resolved) {
1664
+ const askData = dialogClassifier.consumePendingAskUser();
1665
+ if (askData) {
1666
+ dialogHandled = true;
1667
+ dialogBuffer.length = 0;
1668
+ if (dialogQuiesceTimer) {
1669
+ clearTimeout(dialogQuiesceTimer);
1670
+ dialogQuiesceTimer = void 0;
1671
+ }
1672
+ pauseTimersForInput();
1673
+ logger6.info("AskUserQuestion detected via hook (high confidence, zero delay)", {
1674
+ sessionId,
1675
+ question: askData.question.slice(0, 80),
1676
+ optionCount: askData.options.length
1677
+ });
1678
+ options.onInputRequired({
1679
+ type: "interactive-dialog",
1680
+ content: askData.question,
1681
+ options: askData.options
1682
+ }).then((response) => {
1683
+ resumeTimersAfterInput();
1684
+ dialogHandled = false;
1685
+ if (!resolved && response) {
1686
+ this.terminalManager.write(sessionId, response + "\r");
1687
+ }
1688
+ }).catch((err) => {
1689
+ logger6.warn("onInputRequired callback failed (hook-based)", {
1690
+ error: err.message
1691
+ });
1692
+ resumeTimersAfterInput();
1693
+ dialogHandled = false;
1694
+ });
1695
+ }
1696
+ }
1697
+ });
1698
+ hookWatcher.start();
1699
+ }
866
1700
  const subscription = this.terminalManager.onData(sessionId, (data) => {
867
1701
  if (resolved) return;
868
1702
  const stripped = stripAnsi(data);
@@ -877,7 +1711,7 @@ var PtyRunner = class {
877
1711
  clearTimeout(dialogQuiesceTimer);
878
1712
  dialogQuiesceTimer = void 0;
879
1713
  pendingDialogParsed = null;
880
- logger3.info("Dialog quiesce cancelled \u2014 new substantive output arrived", { sessionId });
1714
+ logger6.info("Dialog quiesce cancelled \u2014 new substantive output arrived", { sessionId });
881
1715
  }
882
1716
  }
883
1717
  if (!echoConsumed && stripped.includes(".phase-prompt.md")) {
@@ -888,27 +1722,27 @@ var PtyRunner = class {
888
1722
  return;
889
1723
  }
890
1724
  if (options.completionSignal && hasSubstantiveOutput && options.completionSignal.test(stripped)) {
891
- logger3.info("Completion signal detected", { sessionId });
1725
+ logger6.info("Completion signal detected", { sessionId });
892
1726
  finish({ output: outputLines.join(""), timedOut: false });
893
1727
  return;
894
1728
  }
895
1729
  if (hasSubstantiveOutput && WORKED_SUMMARY_RE.test(stripped)) {
896
1730
  const artifactReady = options.artifactCheck ? options.artifactCheck() : true;
897
1731
  if (artifactReady) {
898
- logger3.info("Session-end summary detected, finishing", { sessionId });
1732
+ logger6.info("Session-end summary detected, finishing", { sessionId });
899
1733
  finish({ output: outputLines.join(""), timedOut: false });
900
1734
  return;
901
1735
  }
902
- logger3.info("Session summary detected but artifacts not ready, continuing", { sessionId });
1736
+ logger6.info("Session summary detected but artifacts not ready, continuing", { sessionId });
903
1737
  }
904
1738
  if (PERMISSION_DIALOG_RE.test(stripped)) {
905
- logger3.info("Permission dialog detected, auto-confirming", { sessionId });
1739
+ logger6.info("Permission dialog detected, auto-confirming", { sessionId });
906
1740
  setTimeout(() => {
907
1741
  if (!resolved) this.terminalManager.write(sessionId, "\r");
908
1742
  }, 500);
909
1743
  return;
910
1744
  }
911
- if (options.onInputRequired && !dialogHandled && isInteractiveDialog(stripped)) {
1745
+ if (options.onInputRequired && !dialogHandled && !dialogClassifier.shouldSuppressRegex() && isInteractiveDialog(stripped)) {
912
1746
  const parsed = parseInteractiveDialog(stripped);
913
1747
  if (parsed) {
914
1748
  const confidence = getDialogConfidence(stripped);
@@ -919,7 +1753,8 @@ var PtyRunner = class {
919
1753
  clearTimeout(dialogQuiesceTimer);
920
1754
  dialogQuiesceTimer = void 0;
921
1755
  }
922
- logger3.info("Interactive dialog detected (high confidence), forwarding to handler", {
1756
+ pauseTimersForInput();
1757
+ logger6.info("Interactive dialog detected (high confidence), forwarding to handler", {
923
1758
  sessionId,
924
1759
  question: parsed.question.slice(0, 80),
925
1760
  optionCount: parsed.options.length
@@ -929,21 +1764,23 @@ var PtyRunner = class {
929
1764
  content: parsed.question,
930
1765
  options: parsed.options
931
1766
  }).then((response) => {
1767
+ resumeTimersAfterInput();
932
1768
  dialogHandled = false;
933
1769
  if (!resolved && response) {
934
1770
  this.terminalManager.write(sessionId, response + "\r");
935
1771
  }
936
1772
  }).catch((err) => {
937
- logger3.warn("onInputRequired callback failed for interactive dialog", {
1773
+ logger6.warn("onInputRequired callback failed for interactive dialog", {
938
1774
  error: err.message
939
1775
  });
1776
+ resumeTimersAfterInput();
940
1777
  dialogHandled = false;
941
1778
  });
942
1779
  return;
943
1780
  }
944
1781
  if (!dialogQuiesceTimer) {
945
1782
  pendingDialogParsed = parsed;
946
- logger3.info("Interactive dialog detected (low confidence), starting quiesce", {
1783
+ logger6.info("Interactive dialog detected (low confidence), starting quiesce", {
947
1784
  sessionId,
948
1785
  question: parsed.question.slice(0, 80),
949
1786
  optionCount: parsed.options.length
@@ -955,7 +1792,8 @@ var PtyRunner = class {
955
1792
  dialogBuffer.length = 0;
956
1793
  const dp = pendingDialogParsed;
957
1794
  pendingDialogParsed = null;
958
- logger3.info("Dialog quiesce elapsed \u2014 forwarding low-confidence dialog", {
1795
+ pauseTimersForInput();
1796
+ logger6.info("Dialog quiesce elapsed \u2014 forwarding low-confidence dialog", {
959
1797
  sessionId,
960
1798
  question: dp.question.slice(0, 80)
961
1799
  });
@@ -964,14 +1802,16 @@ var PtyRunner = class {
964
1802
  content: dp.question,
965
1803
  options: dp.options
966
1804
  }).then((response) => {
1805
+ resumeTimersAfterInput();
967
1806
  dialogHandled = false;
968
1807
  if (!resolved && response) {
969
1808
  this.terminalManager.write(sessionId, response + "\r");
970
1809
  }
971
1810
  }).catch((err) => {
972
- logger3.warn("onInputRequired callback failed (quiesced dialog)", {
1811
+ logger6.warn("onInputRequired callback failed (quiesced dialog)", {
973
1812
  error: err.message
974
1813
  });
1814
+ resumeTimersAfterInput();
975
1815
  dialogHandled = false;
976
1816
  });
977
1817
  }, DIALOG_QUIESCE_MS);
@@ -1005,10 +1845,10 @@ var PtyRunner = class {
1005
1845
  if (hasSubstantiveOutput && isIdlePrompt(stripped)) {
1006
1846
  if (isMixedFrame) {
1007
1847
  } else if (options.completionSignal) {
1008
- logger3.debug("Idle prompt ignored (waiting for completionSignal)", { sessionId });
1848
+ logger6.debug("Idle prompt ignored (waiting for completionSignal)", { sessionId });
1009
1849
  } else if (debounceTimer && isNoise) {
1010
1850
  } else {
1011
- if (options.onInputRequired && !dialogHandled && dialogBuffer.length >= 2) {
1851
+ if (options.onInputRequired && !dialogHandled && !dialogClassifier.shouldSuppressRegex() && dialogBuffer.length >= 2) {
1012
1852
  const combined = dialogBuffer.join("\n");
1013
1853
  if (isInteractiveDialog(combined)) {
1014
1854
  const parsed = parseInteractiveDialog(combined);
@@ -1021,7 +1861,8 @@ var PtyRunner = class {
1021
1861
  clearTimeout(dialogQuiesceTimer);
1022
1862
  dialogQuiesceTimer = void 0;
1023
1863
  }
1024
- logger3.info("Interactive dialog detected via accumulated buffer (high confidence)", {
1864
+ pauseTimersForInput();
1865
+ logger6.info("Interactive dialog detected via accumulated buffer (high confidence)", {
1025
1866
  sessionId,
1026
1867
  question: parsed.question.slice(0, 80),
1027
1868
  optionCount: parsed.options.length
@@ -1031,21 +1872,23 @@ var PtyRunner = class {
1031
1872
  content: parsed.question,
1032
1873
  options: parsed.options
1033
1874
  }).then((response) => {
1875
+ resumeTimersAfterInput();
1034
1876
  dialogHandled = false;
1035
1877
  if (!resolved && response) {
1036
1878
  this.terminalManager.write(sessionId, response + "\r");
1037
1879
  }
1038
1880
  }).catch((err) => {
1039
- logger3.warn("onInputRequired callback failed (accumulated)", {
1881
+ logger6.warn("onInputRequired callback failed (accumulated)", {
1040
1882
  error: err.message
1041
1883
  });
1884
+ resumeTimersAfterInput();
1042
1885
  dialogHandled = false;
1043
1886
  });
1044
1887
  return;
1045
1888
  }
1046
1889
  if (!dialogQuiesceTimer) {
1047
1890
  pendingDialogParsed = parsed;
1048
- logger3.info("Dialog detected via buffer (low confidence), starting quiesce", {
1891
+ logger6.info("Dialog detected via buffer (low confidence), starting quiesce", {
1049
1892
  sessionId,
1050
1893
  question: parsed.question.slice(0, 80)
1051
1894
  });
@@ -1056,20 +1899,23 @@ var PtyRunner = class {
1056
1899
  dialogBuffer.length = 0;
1057
1900
  const dp = pendingDialogParsed;
1058
1901
  pendingDialogParsed = null;
1059
- logger3.info("Buffer dialog quiesce elapsed \u2014 forwarding", { sessionId });
1902
+ pauseTimersForInput();
1903
+ logger6.info("Buffer dialog quiesce elapsed \u2014 forwarding", { sessionId });
1060
1904
  options.onInputRequired({
1061
1905
  type: "interactive-dialog",
1062
1906
  content: dp.question,
1063
1907
  options: dp.options
1064
1908
  }).then((response) => {
1909
+ resumeTimersAfterInput();
1065
1910
  dialogHandled = false;
1066
1911
  if (!resolved && response) {
1067
1912
  this.terminalManager.write(sessionId, response + "\r");
1068
1913
  }
1069
1914
  }).catch((err) => {
1070
- logger3.warn("onInputRequired callback failed (buffer quiesced)", {
1915
+ logger6.warn("onInputRequired callback failed (buffer quiesced)", {
1071
1916
  error: err.message
1072
1917
  });
1918
+ resumeTimersAfterInput();
1073
1919
  dialogHandled = false;
1074
1920
  });
1075
1921
  }, DIALOG_QUIESCE_MS);
@@ -1088,7 +1934,7 @@ var PtyRunner = class {
1088
1934
  }
1089
1935
  const artifactReady = options.artifactCheck ? options.artifactCheck() : true;
1090
1936
  if (!artifactReady) {
1091
- logger3.info("Idle prompt detected but artifacts not ready, continuing to wait", {
1937
+ logger6.info("Idle prompt detected but artifacts not ready, continuing to wait", {
1092
1938
  sessionId
1093
1939
  });
1094
1940
  scheduleDebounce();
@@ -1114,14 +1960,14 @@ var PtyRunner = class {
1114
1960
  }
1115
1961
  }
1116
1962
  if (!resolved) {
1117
- logger3.warn("PTY process exited during phase", { sessionId, exitCode, wasKilled });
1963
+ logger6.warn("PTY process exited during phase", { sessionId, exitCode, wasKilled });
1118
1964
  finish({
1119
1965
  output: outputLines.join(""),
1120
1966
  timedOut: wasKilled,
1121
1967
  timeoutType: wasKilled ? "wall-clock" : void 0
1122
1968
  });
1123
1969
  } else {
1124
- logger3.info("PTY exited after phase completion (post stop-hook)", { sessionId, exitCode });
1970
+ logger6.info("PTY exited after phase completion (post stop-hook)", { sessionId, exitCode });
1125
1971
  }
1126
1972
  });
1127
1973
  const cleanup = () => {
@@ -1130,6 +1976,9 @@ var PtyRunner = class {
1130
1976
  if (debounceTimer) clearTimeout(debounceTimer);
1131
1977
  if (dialogQuiesceTimer) clearTimeout(dialogQuiesceTimer);
1132
1978
  subscription.dispose();
1979
+ hookSub?.dispose();
1980
+ hookWatcher?.stop();
1981
+ dialogClassifier.reset();
1133
1982
  };
1134
1983
  });
1135
1984
  }
@@ -1137,6 +1986,7 @@ var PtyRunner = class {
1137
1986
 
1138
1987
  export {
1139
1988
  PlanFileResolver,
1989
+ HookInjector,
1140
1990
  stripAnsi,
1141
1991
  isIdlePrompt,
1142
1992
  TRUST_DIALOG_RE,
@@ -1148,6 +1998,7 @@ export {
1148
1998
  isInteractiveDialog,
1149
1999
  containsActiveWork,
1150
2000
  isTuiNoise,
2001
+ InputWaitController,
1151
2002
  PtyRunner
1152
2003
  };
1153
- //# sourceMappingURL=chunk-4XMYOXGZ.js.map
2004
+ //# sourceMappingURL=chunk-36G3DPO3.js.map