@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.
Files changed (41) hide show
  1. package/dist/{PtyRunner-NYASBTRP.js → PtyRunner-XMWDMH3L.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/PtyRunner.d.ts +17 -0
  5. package/dist/ai-runner/PtyRunner.d.ts.map +1 -1
  6. package/dist/{ai-runner-TOHVJJ76.js → ai-runner-S2ATTGWX.js} +2 -2
  7. package/dist/{analyze-DBH4K3J7.js → analyze-DAVYPBHK.js} +2 -2
  8. package/dist/{braindump-RYI4BGMG.js → braindump-A4R3A4QT.js} +2 -2
  9. package/dist/{chunk-ENF24C44.js → chunk-2XACBKPB.js} +2 -2
  10. package/dist/{chunk-6T7ZHAV2.js → chunk-BPVRMZU4.js} +9 -9
  11. package/dist/{chunk-4XMYOXGZ.js → chunk-HD6V7KPE.js} +878 -67
  12. package/dist/chunk-HD6V7KPE.js.map +1 -0
  13. package/dist/{chunk-WZGEYHCC.js → chunk-OPWP73PW.js} +271 -716
  14. package/dist/chunk-OPWP73PW.js.map +1 -0
  15. package/dist/{chunk-2WDVTLVF.js → chunk-SNSEW7DS.js} +1 -1
  16. package/dist/cli.js +5 -5
  17. package/dist/hooks/HookInjector.d.ts +14 -0
  18. package/dist/hooks/HookInjector.d.ts.map +1 -1
  19. package/dist/index.js +4 -4
  20. package/dist/{init-UKTP7LXS.js → init-OD7CLRWK.js} +2 -2
  21. package/dist/lib.js +2 -2
  22. package/dist/{restart-5D3ZDD5L.js → restart-JVVOYC6C.js} +2 -2
  23. package/dist/run.js +4 -4
  24. package/dist/{start-IQBNXLEI.js → start-INU24RRG.js} +2 -2
  25. package/package.json +1 -1
  26. package/src/web/frontend/dist/assets/index-BrvoaFSK.css +1 -0
  27. package/src/web/frontend/dist/assets/{index-BR0UoQER.js → index-CmyxgdS_.js} +54 -54
  28. package/src/web/frontend/dist/index.html +2 -2
  29. package/dist/chunk-4XMYOXGZ.js.map +0 -1
  30. package/dist/chunk-WZGEYHCC.js.map +0 -1
  31. package/src/web/frontend/dist/assets/index-DWOHf3bd.css +0 -1
  32. /package/dist/{PtyRunner-NYASBTRP.js.map → PtyRunner-XMWDMH3L.js.map} +0 -0
  33. /package/dist/{ai-runner-TOHVJJ76.js.map → ai-runner-S2ATTGWX.js.map} +0 -0
  34. /package/dist/{analyze-DBH4K3J7.js.map → analyze-DAVYPBHK.js.map} +0 -0
  35. /package/dist/{braindump-RYI4BGMG.js.map → braindump-A4R3A4QT.js.map} +0 -0
  36. /package/dist/{chunk-ENF24C44.js.map → chunk-2XACBKPB.js.map} +0 -0
  37. /package/dist/{chunk-6T7ZHAV2.js.map → chunk-BPVRMZU4.js.map} +0 -0
  38. /package/dist/{chunk-2WDVTLVF.js.map → chunk-SNSEW7DS.js.map} +0 -0
  39. /package/dist/{init-UKTP7LXS.js.map → init-OD7CLRWK.js.map} +0 -0
  40. /package/dist/{restart-5D3ZDD5L.js.map → restart-JVVOYC6C.js.map} +0 -0
  41. /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 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";
@@ -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 logger3 = logger.child("PtyRunner");
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
- logger3.warn("PtyRunner skipped \u2014 service is shutting down");
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
- logger3.info("PtyRunner.run()", { workDir, timeoutMs, phaseName, agentMode, continueSession });
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
- logger3.info("Waiting for AI agent prompt (new session)", { sessionId, phaseName });
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
- logger3.info("Waiting for AI agent prompt (continue-session)", { sessionId, phaseName });
1092
+ logger6.info("Waiting for AI agent prompt (continue-session)", { sessionId, phaseName });
374
1093
  await this.waitForPrompt(sessionId, 3e4);
375
1094
  } else {
376
- logger3.info("Waiting for AI agent prompt (reused session)", { sessionId, phaseName });
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
- logger3.info("Continue-session mode: attaching detectCompletion (no /clear)", { sessionId });
1102
+ logger6.info("Continue-session mode: attaching detectCompletion (no /clear)", { sessionId });
384
1103
  const result2 = await this.detectCompletion(sessionId, options, onStreamEvent);
385
- logger3.info("PtyRunner continue-session completed", {
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
- logger3.info("PtyRunner phase completed", {
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
- logger3.info("PtyRunner: all managed sessions destroyed");
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
- logger3.info("Interrupted PTY session for retry", {
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
- logger3.info("Agent switched, destroying old PTY session", {
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
- logger3.info("Reusing existing PTY session (same agent)", {
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
- logger3.info("Destroying orphaned PTY session on workDir", {
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
- logger3.info("Created new PTY session", {
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
- logger3.warn("Timed out waiting for AI agent prompt", { sessionId, timeoutMs });
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
- logger3.info("AI agent prompt detected", { sessionId, reason });
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
- logger3.info("Banner shown and PTY silent \u2014 treating agent as ready", { sessionId });
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
- logger3.info("Trust dialog detected, auto-confirming", { sessionId });
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
- logger3.info("Permission dialog detected in waitForPrompt, auto-confirming", { sessionId });
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
- logger3.info("PTY already in target mode", { sessionId, targetMode });
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
- logger3.info("PTY mode switched", {
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
- logger3.warn("Failed to switch PTY mode after max attempts", {
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
- logger3.info("Native plan mode: continue-session (no /clear, no prompt)", { sessionId });
1434
+ logger6.info("Native plan mode: continue-session (no /clear, no prompt)", { sessionId });
716
1435
  } else {
717
- logger3.info("Native plan mode: switching to plan", { sessionId, agentMode });
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
- logger3.warn("Native plan mode: plan phase timed out", {
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
- logger3.info("Native plan mode: resolving plan file from CLI storage", { sessionId });
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
- logger3.error("Native plan mode: no plan file found in CLI storage", { sessionId, workDir });
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 = path2.dirname(targetPath);
759
- if (!fs2.existsSync(targetDir)) {
760
- fs2.mkdirSync(targetDir, { recursive: true });
1477
+ const targetDir = path3.dirname(targetPath);
1478
+ if (!fs4.existsSync(targetDir)) {
1479
+ fs4.mkdirSync(targetDir, { recursive: true });
761
1480
  }
762
- fs2.writeFileSync(targetPath, resolved.content, "utf-8");
763
- logger3.info("Plan file copied to artifact path", {
764
- source: path2.basename(resolved.sourcePath),
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
- logger3.warn("Native plan mode: no artifactPaths specified, plan file not copied", {
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
- logger3.info("Native plan mode completed (deterministic copy)", {
1494
+ logger6.info("Native plan mode completed (deterministic copy)", {
776
1495
  sessionId,
777
- planSource: path2.basename(resolved.sourcePath),
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 = path2.join(workDir, ".claude-plan");
796
- if (!fs2.existsSync(dir)) {
797
- fs2.mkdirSync(dir, { recursive: true });
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 = path2.join(workDir, relPath);
801
- fs2.writeFileSync(absPath, prompt, "utf-8");
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
- logger3.info("Wall-clock timeout extended (agent still active)", {
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
- logger3.info("Dialog quiesce cancelled \u2014 new substantive output arrived", { sessionId });
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
- logger3.info("Completion signal detected", { sessionId });
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
- logger3.info("Session-end summary detected, finishing", { sessionId });
1692
+ logger6.info("Session-end summary detected, finishing", { sessionId });
899
1693
  finish({ output: outputLines.join(""), timedOut: false });
900
1694
  return;
901
1695
  }
902
- logger3.info("Session summary detected but artifacts not ready, continuing", { sessionId });
1696
+ logger6.info("Session summary detected but artifacts not ready, continuing", { sessionId });
903
1697
  }
904
1698
  if (PERMISSION_DIALOG_RE.test(stripped)) {
905
- logger3.info("Permission dialog detected, auto-confirming", { sessionId });
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
- logger3.info("Interactive dialog detected (high confidence), forwarding to handler", {
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
- logger3.warn("onInputRequired callback failed for interactive dialog", {
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
- logger3.info("Interactive dialog detected (low confidence), starting quiesce", {
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
- logger3.info("Dialog quiesce elapsed \u2014 forwarding low-confidence dialog", {
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
- logger3.warn("onInputRequired callback failed (quiesced dialog)", {
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
- logger3.debug("Idle prompt ignored (waiting for completionSignal)", { sessionId });
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
- logger3.info("Interactive dialog detected via accumulated buffer (high confidence)", {
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
- logger3.warn("onInputRequired callback failed (accumulated)", {
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
- logger3.info("Dialog detected via buffer (low confidence), starting quiesce", {
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
- logger3.info("Buffer dialog quiesce elapsed \u2014 forwarding", { sessionId });
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
- logger3.warn("onInputRequired callback failed (buffer quiesced)", {
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
- logger3.info("Idle prompt detected but artifacts not ready, continuing to wait", {
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
- logger3.warn("PTY process exited during phase", { sessionId, exitCode, wasKilled });
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
- logger3.info("PTY exited after phase completion (post stop-hook)", { sessionId, exitCode });
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-4XMYOXGZ.js.map
1964
+ //# sourceMappingURL=chunk-HD6V7KPE.js.map