@scotthuang/agent-knock-knock 0.12.2 → 0.12.4

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.
@@ -28,6 +28,16 @@ const CODEX_NATIVE_STATUS_POPUP_BY_PROFILE = {
28
28
  " /statusline configure which items appear in the status line"
29
29
  ]
30
30
  };
31
+ // Codex's verified `/status` profiles are exercised against its canonical
32
+ // 80-column status surface. Narrower layouts can truncate the 36-character
33
+ // Session UUID after a dynamically sized label column. Exact provider geometry
34
+ // is required before input; ANSI visible-buffer width is only a conservative
35
+ // fallback diagnostic and never upgrades unknown geometry to safe.
36
+ const CODEX_NATIVE_STATUS_MIN_VIEWPORT_BY_PROFILE = {
37
+ "codex-tui-0.146.0": 80,
38
+ "codex-tui-0.146.1": 80,
39
+ "codex-tui-0.147.0": 80
40
+ };
31
41
  const CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE = {
32
42
  "claude-code-2.1.218-native-status": [
33
43
  "/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
@@ -67,6 +77,16 @@ export class NativeInspectionSubmissionError extends Error {
67
77
  this.stage = stage;
68
78
  this.name = "NativeInspectionSubmissionError";
69
79
  this.doNotRetry = stage !== "not_started";
80
+ this.diagnostic = options.diagnostic;
81
+ }
82
+ diagnostic;
83
+ }
84
+ class NativeInspectionDiagnosticError extends Error {
85
+ diagnostic;
86
+ constructor(diagnostic, message, options = {}) {
87
+ super(message, options);
88
+ this.diagnostic = diagnostic;
89
+ this.name = "NativeInspectionDiagnosticError";
70
90
  }
71
91
  }
72
92
  /** A verified modal could not be dismissed across one exact key attempt. */
@@ -376,6 +396,49 @@ export class TerminalAgentBridge {
376
396
  * draft untouched instead of issuing the legacy best-effort C-u cleanup.
377
397
  */
378
398
  async submitNativeInspection(agent, terminalControl, plan, options = {}) {
399
+ return this.submitClosedNativeInspection(agent, terminalControl, plan, options, { requireCodexReadyComposer: agent === "codex" });
400
+ }
401
+ /**
402
+ * Submit Codex's closed, version-profiled `/status` probe.
403
+ *
404
+ * Unlike the generic native-inspection entry point, callers provide only
405
+ * the detected Codex version, never a command or plan. The bridge proves an
406
+ * exact empty (or fully dim replace-on-type) ANSI composer before injecting
407
+ * text, then crosses Codex's paste suppression window under the same exact
408
+ * composer and terminal-identity fences used by native inspection.
409
+ */
410
+ async submitCodexStatusProbe(terminalControl, agentVersion, options = {}) {
411
+ const adapter = this.registry.require("codex");
412
+ let plan;
413
+ try {
414
+ const capability = adapter.probeNativeInspection?.(agentVersion);
415
+ if (capability?.status !== "supported" ||
416
+ capability.statusInspection !== true) {
417
+ throw new NativeInspectionDiagnosticError("unsupported_profile", capability?.reason ??
418
+ `Codex ${agentVersion} has no closed /status behavior profile`);
419
+ }
420
+ const planned = adapter.planNativeInspection?.({ kind: "status" }, capability);
421
+ if (!planned) {
422
+ throw new NativeInspectionDiagnosticError("unsupported_profile", `Codex ${agentVersion} did not produce a closed /status plan`);
423
+ }
424
+ plan = planned;
425
+ }
426
+ catch (error) {
427
+ throw nativeInspectionSubmissionError("not_started", error, "unsupported_profile");
428
+ }
429
+ const result = await this.submitClosedNativeInspection("codex", terminalControl, plan, options, { requireCodexReadyComposer: true });
430
+ if (!result.preTextScreenDigest) {
431
+ throw new Error("Codex /status pre-text composer evidence is missing");
432
+ }
433
+ return {
434
+ ...result,
435
+ agent: "codex",
436
+ preTextScreenDigest: result.preTextScreenDigest,
437
+ observationBaselineDigest: bareDigestFromNativeInspectionScreenFingerprint(result.preEnterScreenDigest),
438
+ observationScrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
439
+ };
440
+ }
441
+ async submitClosedNativeInspection(agent, terminalControl, plan, options, safety = {}) {
379
442
  const adapter = this.registry.require(agent);
380
443
  try {
381
444
  assertClosedStatusInspectionPlan(adapter, terminalControl, plan);
@@ -387,19 +450,33 @@ export class TerminalAgentBridge {
387
450
  "stable_resource_resolution",
388
451
  "screen_capture",
389
452
  "text_delivery",
390
- "key_delivery"
453
+ "key_delivery",
454
+ ...(safety.requireCodexReadyComposer
455
+ ? ["ansi_capture"]
456
+ : [])
391
457
  ]
392
458
  });
393
459
  }
394
460
  catch (error) {
395
- throw nativeInspectionSubmissionError("not_started", error);
461
+ throw nativeInspectionSubmissionError("not_started", error, "capability_unavailable");
396
462
  }
397
463
  let verifiedForText;
398
464
  try {
399
465
  verifiedForText = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
400
466
  }
401
467
  catch (error) {
402
- throw nativeInspectionSubmissionError("not_started", error);
468
+ throw nativeInspectionSubmissionError("not_started", error, "identity_unverified");
469
+ }
470
+ let preTextScreenDigest;
471
+ if (safety.requireCodexReadyComposer) {
472
+ try {
473
+ const ready = await this.captureCodexReadyComposer(adapter, verifiedForText, plan, options.runtime);
474
+ verifiedForText = ready.terminalControl;
475
+ preTextScreenDigest = ready.screenDigest;
476
+ }
477
+ catch (error) {
478
+ throw nativeInspectionSubmissionError("not_started", error, "composer_not_ready");
479
+ }
403
480
  }
404
481
  try {
405
482
  if (!verifiedForText.capabilities.includes("send_keys")) {
@@ -409,11 +486,11 @@ export class TerminalAgentBridge {
409
486
  }
410
487
  catch (error) {
411
488
  if (error instanceof TerminalControlInputNotSentError) {
412
- throw nativeInspectionSubmissionError("not_started", error);
489
+ throw nativeInspectionSubmissionError("not_started", error, "text_delivery_unproven");
413
490
  }
414
491
  // The transport cannot prove whether an untyped failure happened before
415
492
  // or after tmux accepted the literal input. Fail closed as injected.
416
- throw nativeInspectionSubmissionError("text_injected", error);
493
+ throw nativeInspectionSubmissionError("text_injected", error, "text_delivery_unproven");
417
494
  }
418
495
  let settled;
419
496
  try {
@@ -425,10 +502,20 @@ export class TerminalAgentBridge {
425
502
  preEnterScreenDigest: settled.screenDigest,
426
503
  materialization: settled.materialization
427
504
  });
505
+ if (safety.requireCodexReadyComposer) {
506
+ settled = {
507
+ ...settled,
508
+ terminalControl: await this.assertFinalCodexStatusViewport(settled.terminalControl, plan, options.runtime)
509
+ };
510
+ }
511
+ // Viewport inspection may await provider I/O and the PTY may receive
512
+ // human input while that proof is in flight. Keep the exact composer
513
+ // capture as the final substantive asynchronous evidence before the
514
+ // single Enter attempt.
428
515
  settled = await this.revalidateNativeInspectionComposer(adapter, settled.terminalControl, plan, settled.materialization, options.runtime);
429
516
  }
430
517
  catch (error) {
431
- throw nativeInspectionSubmissionError("text_injected", error);
518
+ throw nativeInspectionSubmissionError("text_injected", error, "composer_not_exact");
432
519
  }
433
520
  try {
434
521
  // Exactly one Enter attempt. Any error is submission-uncertain and must
@@ -436,7 +523,7 @@ export class TerminalAgentBridge {
436
523
  await this.terminalProvider.sendKeys(this.terminalProvider.endpoint(settled.terminalControl), ["C-m"]);
437
524
  }
438
525
  catch (error) {
439
- throw nativeInspectionSubmissionError("enter_uncertain", error);
526
+ throw nativeInspectionSubmissionError("enter_uncertain", error, "enter_uncertain");
440
527
  }
441
528
  return {
442
529
  stage: "enter_dispatched",
@@ -447,7 +534,8 @@ export class TerminalAgentBridge {
447
534
  preEnterScreenDigest: settled.screenDigest,
448
535
  preEnterEvidenceInventory: settled.evidenceInventory,
449
536
  materialization: settled.materialization,
450
- enterCount: 1
537
+ enterCount: 1,
538
+ ...(preTextScreenDigest ? { preTextScreenDigest } : {})
451
539
  };
452
540
  }
453
541
  async observeNativeInspection(agent, terminalControl, request, options = {}) {
@@ -556,6 +644,121 @@ export class TerminalAgentBridge {
556
644
  throw new NativeInspectionDismissalError(error instanceof Error ? error.message : String(error), { cause: error });
557
645
  }
558
646
  }
647
+ async captureCodexReadyComposer(adapter, terminalControl, plan, runtime) {
648
+ if (adapter.agent !== "codex") {
649
+ throw new NativeInspectionDiagnosticError("unsupported_profile", "the closed Codex /status probe requires the Codex adapter");
650
+ }
651
+ const minimumViewport = CODEX_NATIVE_STATUS_MIN_VIEWPORT_BY_PROFILE[plan.behaviorProfile];
652
+ if (minimumViewport === undefined) {
653
+ throw new NativeInspectionDiagnosticError("unsupported_profile", `Codex ${plan.behaviorProfile} has no exact /status viewport profile`);
654
+ }
655
+ const captureReady = async (control) => {
656
+ const verified = await this.verifyTerminalIdentity(adapter.agent, control, runtime);
657
+ const endpoint = this.terminalProvider.endpoint(verified);
658
+ const viewportInspector = this.terminalProvider.inspectViewport;
659
+ let exactViewport;
660
+ let viewportUnavailableReason = "terminal provider has no exact viewport inspector";
661
+ if (viewportInspector) {
662
+ let viewport;
663
+ try {
664
+ viewport = await viewportInspector.call(this.terminalProvider, endpoint);
665
+ }
666
+ catch (error) {
667
+ throw new NativeInspectionDiagnosticError("viewport_unavailable", `Codex /status viewport inspection failed before terminal input: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
668
+ }
669
+ if (viewport) {
670
+ if (!Number.isSafeInteger(viewport.columns) ||
671
+ viewport.columns <= 0 ||
672
+ !Number.isSafeInteger(viewport.rows) ||
673
+ viewport.rows <= 0) {
674
+ throw new NativeInspectionDiagnosticError("viewport_unavailable", "Codex /status viewport inspector returned invalid geometry");
675
+ }
676
+ exactViewport = viewport.columns;
677
+ }
678
+ else {
679
+ viewportUnavailableReason =
680
+ "terminal provider could not prove exact viewport geometry";
681
+ }
682
+ }
683
+ const styledScreen = await this.terminalProvider.capture(endpoint, { scrollbackLines: 40, preserveEscapes: true });
684
+ const plainScreen = stripTerminalEscapeSequences(styledScreen);
685
+ const inspection = adapter.inspectScreen({ screen: plainScreen, runtime });
686
+ assertNativeInspectionComposerSafe(inspection, adapter.displayName);
687
+ const composer = exactCodexReadyStyledComposerCapture(styledScreen);
688
+ if (!composer) {
689
+ throw new NativeInspectionDiagnosticError("composer_not_ready", "Codex composer contains non-placeholder input or is not at the exact idle prompt");
690
+ }
691
+ const inferredViewport = inferCodexVisibleViewportColumns(styledScreen);
692
+ const observedViewport = exactViewport ?? inferredViewport;
693
+ if ((observedViewport !== undefined && observedViewport < minimumViewport) ||
694
+ hasTruncatedCodexStatusSessionLine(plainScreen)) {
695
+ throw new NativeInspectionDiagnosticError("viewport_too_narrow", `Codex /status requires a proven viewport of at least ` +
696
+ `${minimumViewport} columns to preserve the complete Session UUID` +
697
+ `${observedViewport === undefined
698
+ ? ""
699
+ : `; observed ${observedViewport}`}; widen or zoom the pane before retrying`);
700
+ }
701
+ if (exactViewport === undefined) {
702
+ throw new NativeInspectionDiagnosticError("viewport_unavailable", `Codex /status requires exact terminal viewport geometry before input; ` +
703
+ viewportUnavailableReason +
704
+ `${inferredViewport === undefined
705
+ ? ""
706
+ : ` (ANSI fallback estimated ${inferredViewport} columns)`}`);
707
+ }
708
+ const reverified = await this.verifyTerminalIdentity(adapter.agent, verified, runtime);
709
+ if (!sameTerminalControlIdentity(verified, reverified)) {
710
+ throw new NativeInspectionDiagnosticError("identity_unverified", "terminal control identity changed after the Codex pre-text composer capture");
711
+ }
712
+ return {
713
+ terminalControl: reverified,
714
+ screenDigest: nativeInspectionScreenFingerprint(styledScreen),
715
+ composerDigest: composer.digest
716
+ };
717
+ };
718
+ const first = await captureReady(terminalControl);
719
+ const second = await captureReady(first.terminalControl);
720
+ if (second.composerDigest !== first.composerDigest) {
721
+ throw new NativeInspectionDiagnosticError("composer_not_ready", "Codex empty composer changed across its stable pre-text captures");
722
+ }
723
+ return {
724
+ terminalControl: second.terminalControl,
725
+ screenDigest: second.screenDigest
726
+ };
727
+ }
728
+ async assertFinalCodexStatusViewport(terminalControl, plan, runtime) {
729
+ const minimumViewport = CODEX_NATIVE_STATUS_MIN_VIEWPORT_BY_PROFILE[plan.behaviorProfile];
730
+ if (minimumViewport === undefined) {
731
+ throw new NativeInspectionDiagnosticError("unsupported_profile", `Codex ${plan.behaviorProfile} has no exact /status viewport profile`);
732
+ }
733
+ const verified = await this.verifyTerminalIdentity("codex", terminalControl, runtime);
734
+ const viewportInspector = this.terminalProvider.inspectViewport;
735
+ if (!viewportInspector) {
736
+ throw new NativeInspectionDiagnosticError("viewport_unavailable", "Codex /status requires exact terminal viewport geometry immediately before Enter");
737
+ }
738
+ let viewport;
739
+ try {
740
+ viewport = await viewportInspector.call(this.terminalProvider, this.terminalProvider.endpoint(verified));
741
+ }
742
+ catch (error) {
743
+ throw new NativeInspectionDiagnosticError("viewport_unavailable", `Codex /status viewport inspection failed immediately before Enter: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
744
+ }
745
+ if (!viewport ||
746
+ !Number.isSafeInteger(viewport.columns) ||
747
+ viewport.columns <= 0 ||
748
+ !Number.isSafeInteger(viewport.rows) ||
749
+ viewport.rows <= 0) {
750
+ throw new NativeInspectionDiagnosticError("viewport_unavailable", "Codex /status exact terminal viewport became unavailable immediately before Enter");
751
+ }
752
+ if (viewport.columns < minimumViewport) {
753
+ throw new NativeInspectionDiagnosticError("viewport_too_narrow", `Codex /status viewport narrowed to ${viewport.columns} columns before Enter; ` +
754
+ `at least ${minimumViewport} are required to preserve the complete Session UUID`);
755
+ }
756
+ const reverified = await this.verifyTerminalIdentity("codex", verified, runtime);
757
+ if (!sameTerminalControlIdentity(verified, reverified)) {
758
+ throw new NativeInspectionDiagnosticError("identity_unverified", "terminal control identity changed after the final Codex viewport proof");
759
+ }
760
+ return reverified;
761
+ }
559
762
  async settleNativeInspectionComposer(adapter, terminalControl, plan, runtime) {
560
763
  const startedAt = this.nowMs();
561
764
  const settleTimeoutMs = plan.composer.maximumSettleMs;
@@ -563,6 +766,7 @@ export class TerminalAgentBridge {
563
766
  let stableKind;
564
767
  let stableSince;
565
768
  let stableCaptures = 0;
769
+ let lastMismatchDiagnostic = "composer_not_exact";
566
770
  while (this.nowMs() - startedAt <= settleTimeoutMs) {
567
771
  const captured = await this.captureInspection(adapter, terminalControl, {
568
772
  runtime,
@@ -595,6 +799,9 @@ export class TerminalAgentBridge {
595
799
  }
596
800
  }
597
801
  else {
802
+ lastMismatchDiagnostic = adapter.agent === "codex"
803
+ ? codexNativeInspectionComposerMismatchDiagnostic(captured.screen, plan)
804
+ : "composer_not_exact";
598
805
  stableDigest = undefined;
599
806
  stableKind = undefined;
600
807
  stableSince = undefined;
@@ -607,7 +814,9 @@ export class TerminalAgentBridge {
607
814
  }
608
815
  await this.sleep(Math.min(CODEX_MULTILINE_SETTLE_POLL_MS, remaining));
609
816
  }
610
- throw new Error(`${adapter.displayName} /status composer did not become exact, idle, and stable before the bounded submit deadline`);
817
+ throw new NativeInspectionDiagnosticError(lastMismatchDiagnostic, lastMismatchDiagnostic === "composer_viewport_truncated"
818
+ ? `${adapter.displayName} /status slash popup was truncated by the viewport; widen or zoom the pane before retrying manually`
819
+ : `${adapter.displayName} /status composer did not become exact, idle, and stable before the bounded submit deadline`);
611
820
  }
612
821
  async revalidateNativeInspectionComposer(adapter, terminalControl, plan, expected, runtime) {
613
822
  const captured = await this.captureInspection(adapter, terminalControl, {
@@ -619,7 +828,7 @@ export class TerminalAgentBridge {
619
828
  if (!materialized ||
620
829
  materialized.digest !== expected.digest ||
621
830
  materialized.kind !== expected.kind) {
622
- throw new Error(`${adapter.displayName} /status composer changed after its stable pre-submit capture`);
831
+ throw new NativeInspectionDiagnosticError("composer_drift", `${adapter.displayName} /status composer changed after its stable pre-submit capture`);
623
832
  }
624
833
  const baseline = adapter.observeNativeInspection?.({
625
834
  operation: plan.operation,
@@ -628,12 +837,12 @@ export class TerminalAgentBridge {
628
837
  if (!baseline ||
629
838
  baseline.status === "ambiguous" ||
630
839
  !Array.isArray(baseline.evidenceInventory)) {
631
- throw new Error(baseline?.reason ??
840
+ throw new NativeInspectionDiagnosticError("evidence_unproven", baseline?.reason ??
632
841
  `${adapter.displayName} /status pre-Enter evidence inventory was not proven`);
633
842
  }
634
843
  const verifiedImmediatelyBeforeEnter = await this.verifyTerminalIdentity(adapter.agent, captured.terminalControl, runtime);
635
844
  if (!sameTerminalControlIdentity(captured.terminalControl, verifiedImmediatelyBeforeEnter)) {
636
- throw new Error(`terminal control identity changed after the final ${adapter.displayName} /status composer capture`);
845
+ throw new NativeInspectionDiagnosticError("identity_unverified", `terminal control identity changed after the final ${adapter.displayName} /status composer capture`);
637
846
  }
638
847
  return {
639
848
  terminalControl: verifiedImmediatelyBeforeEnter,
@@ -1213,7 +1422,12 @@ function assertTerminalMutationCapabilities({ provider, terminal, semantic, tran
1213
1422
  `${terminal.identity.providerKind}:${terminal.route.label}: ` +
1214
1423
  missing.join(", "));
1215
1424
  }
1216
- function nativeInspectionSubmissionError(stage, error) {
1425
+ function nativeInspectionSubmissionError(stage, error, fallbackDiagnostic) {
1426
+ const diagnostic = error instanceof NativeInspectionSubmissionError
1427
+ ? error.diagnostic
1428
+ : error instanceof NativeInspectionDiagnosticError
1429
+ ? error.diagnostic
1430
+ : fallbackDiagnostic;
1217
1431
  if (error instanceof NativeInspectionSubmissionError) {
1218
1432
  const stageRank = {
1219
1433
  not_started: 0,
@@ -1224,10 +1438,11 @@ function nativeInspectionSubmissionError(stage, error) {
1224
1438
  return error;
1225
1439
  }
1226
1440
  return new NativeInspectionSubmissionError(stage, error.message, {
1227
- cause: error
1441
+ cause: error,
1442
+ diagnostic
1228
1443
  });
1229
1444
  }
1230
- return new NativeInspectionSubmissionError(stage, error instanceof Error ? error.message : String(error), { cause: error });
1445
+ return new NativeInspectionSubmissionError(stage, error instanceof Error ? error.message : String(error), { cause: error, diagnostic });
1231
1446
  }
1232
1447
  function assertClosedStatusInspectionPlan(adapter, terminalControl, plan) {
1233
1448
  if (!terminalControl.capabilities.includes("send_keys")) {
@@ -1286,7 +1501,7 @@ function assertNativeInspectionComposerSafe(inspection, displayName = "terminal
1286
1501
  if (inspection.approval.blocked ||
1287
1502
  inspection.activity.state === "awaiting_approval" ||
1288
1503
  inspection.activity.state === "working") {
1289
- throw new Error(`${displayName} became busy or blocked while its /status composer was settling`);
1504
+ throw new NativeInspectionDiagnosticError("composer_not_ready", `${displayName} became busy or blocked while its /status composer was settling`);
1290
1505
  }
1291
1506
  // Codex's generic activity parser deliberately reports a non-empty slash
1292
1507
  // composer as unknown. At this stage the caller has already proved an idle,
@@ -1341,7 +1556,7 @@ function exactCodexNativeInspectionComposerCapture(screen, plan) {
1341
1556
  };
1342
1557
  }
1343
1558
  function exactClaudeNativeInspectionComposerCapture(screen, plan) {
1344
- const frame = exactClaudeNativeInspectionComposerFrame(screen);
1559
+ const frame = exactClaudeComposerFrame(screen);
1345
1560
  if (!frame) {
1346
1561
  return undefined;
1347
1562
  }
@@ -1409,12 +1624,12 @@ function closedClaudeNativeStatusSuggestionsMatch(observed, expected) {
1409
1624
  });
1410
1625
  }
1411
1626
  /**
1412
- * Prove the exact idle input frame from a verified Claude Code native-status
1413
- * profile. A loose or historical `❯` prompt is not enough to authorize even
1414
- * the initial literal `/status` injection.
1627
+ * Prove Claude Code's exact current idle input frame. This is shared by every
1628
+ * automated-input path: a loose or historical `❯` prompt is not authority to
1629
+ * inject text into the terminal.
1415
1630
  */
1416
- export function isExactClaudeNativeInspectionIdleComposer(screen) {
1417
- const frame = exactClaudeNativeInspectionComposerFrame(screen);
1631
+ export function isExactClaudeIdleComposer(screen) {
1632
+ const frame = exactClaudeComposerFrame(screen);
1418
1633
  if (!frame) {
1419
1634
  return false;
1420
1635
  }
@@ -1423,7 +1638,14 @@ export function isExactClaudeNativeInspectionIdleComposer(screen) {
1423
1638
  (frame.trailing.length === 0 ||
1424
1639
  claudeNativeInspectionTrailingIsFooter(frame.trailing)));
1425
1640
  }
1426
- function exactClaudeNativeInspectionComposerFrame(screen) {
1641
+ /**
1642
+ * Compatibility export retained for callers that adopted the native-status
1643
+ * name before the same exact-frame proof was reused by lifecycle handoff.
1644
+ */
1645
+ export function isExactClaudeNativeInspectionIdleComposer(screen) {
1646
+ return isExactClaudeIdleComposer(screen);
1647
+ }
1648
+ function exactClaudeComposerFrame(screen) {
1427
1649
  const lines = screen.replace(/\r\n?/gu, "\n").replace(/\u00a0/gu, " ")
1428
1650
  .split("\n");
1429
1651
  const dividerIndexes = lines
@@ -1451,6 +1673,162 @@ function claudeNativeInspectionTrailingIsFooter(lines) {
1451
1673
  function nativeInspectionScreenFingerprint(screen) {
1452
1674
  return `sha256:${createHash("sha256").update(screen).digest("hex")}`;
1453
1675
  }
1676
+ function bareDigestFromNativeInspectionScreenFingerprint(fingerprint) {
1677
+ const match = /^sha256:([0-9a-f]{64})$/u.exec(fingerprint);
1678
+ if (!match) {
1679
+ throw new Error("native inspection screen fingerprint is malformed");
1680
+ }
1681
+ return match[1];
1682
+ }
1683
+ function stripTerminalEscapeSequences(value) {
1684
+ return value.replace(/\x1B(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/gu, "");
1685
+ }
1686
+ function exactCodexReadyStyledComposerCapture(screen) {
1687
+ const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
1688
+ while (lines.length > 0 &&
1689
+ stripTerminalEscapeSequences(lines.at(-1) ?? "").trim().length === 0) {
1690
+ lines.pop();
1691
+ }
1692
+ const composerLine = [...lines.slice(-12)].reverse().find((line) => CODEX_COMPOSER_MARKER.test(stripTerminalEscapeSequences(line).trimEnd()));
1693
+ if (composerLine === undefined) {
1694
+ return undefined;
1695
+ }
1696
+ let dim = false;
1697
+ const visible = [];
1698
+ for (let index = 0; index < composerLine.length;) {
1699
+ if (composerLine[index] === "\x1b") {
1700
+ const escape = /^(?:\x1B\[([0-9;]*)m|\x1B\][^\x07]*(?:\x07|\x1B\\))/u
1701
+ .exec(composerLine.slice(index));
1702
+ if (escape) {
1703
+ if (escape[1] !== undefined) {
1704
+ const codes = escape[1] === ""
1705
+ ? [0]
1706
+ : escape[1].split(";").map((value) => Number(value));
1707
+ for (let codeIndex = 0; codeIndex < codes.length; codeIndex += 1) {
1708
+ const code = codes[codeIndex];
1709
+ if ([38, 48, 58].includes(code) &&
1710
+ codes[codeIndex + 1] === 2) {
1711
+ codeIndex += 4;
1712
+ continue;
1713
+ }
1714
+ if ([38, 48, 58].includes(code) &&
1715
+ codes[codeIndex + 1] === 5) {
1716
+ codeIndex += 2;
1717
+ continue;
1718
+ }
1719
+ if (code === 0 || code === 22) {
1720
+ dim = false;
1721
+ }
1722
+ else if (code === 2) {
1723
+ dim = true;
1724
+ }
1725
+ }
1726
+ }
1727
+ index += escape[0].length;
1728
+ continue;
1729
+ }
1730
+ }
1731
+ const codePoint = composerLine.codePointAt(index);
1732
+ if (codePoint === undefined) {
1733
+ break;
1734
+ }
1735
+ const character = String.fromCodePoint(codePoint);
1736
+ visible.push({ character, dim });
1737
+ index += character.length;
1738
+ }
1739
+ const promptIndex = visible.findIndex(({ character }) => character === "›" || character === "»");
1740
+ if (promptIndex < 0) {
1741
+ return undefined;
1742
+ }
1743
+ const content = visible.slice(promptIndex + 1)
1744
+ .filter(({ character }) => !/^\s$/u.test(character));
1745
+ if (content.length > 0 && !content.every((entry) => entry.dim)) {
1746
+ return undefined;
1747
+ }
1748
+ return {
1749
+ digest: createHash("sha256").update(composerLine).digest("hex")
1750
+ };
1751
+ }
1752
+ /**
1753
+ * Infer a viewport only from fixed-width visible-buffer rows. Trimmed captures
1754
+ * deliberately return undefined: a short content row is not proof of a short
1755
+ * terminal. This keeps the fallback provider-neutral and fail-closed only on
1756
+ * positive geometry evidence.
1757
+ */
1758
+ function inferCodexVisibleViewportColumns(screen) {
1759
+ const rows = screen.replace(/\r\n?/gu, "\n").split("\n")
1760
+ .map(stripTerminalEscapeSequences);
1761
+ const widthOneRows = rows.filter((row) => /^[\x20-\x7e›»·─━╭╮╰╯│]*$/u.test(row));
1762
+ const maxWidth = widthOneRows.reduce((maximum, row) => Math.max(maximum, Array.from(row).length), 0);
1763
+ if (maxWidth < 20) {
1764
+ return undefined;
1765
+ }
1766
+ const paddedAtMax = widthOneRows.filter((row) => row.endsWith(" ") && Array.from(row).length === maxWidth);
1767
+ const composerAtMax = paddedAtMax.some((row) => CODEX_COMPOSER_MARKER.test(row.trimEnd()));
1768
+ return paddedAtMax.length >= 3 && composerAtMax
1769
+ ? maxWidth
1770
+ : undefined;
1771
+ }
1772
+ function hasTruncatedCodexStatusSessionLine(screen) {
1773
+ return screen.replace(/\r\n?/gu, "\n").split("\n").some((line) => {
1774
+ const match = /^\s*│\s*Session:\s*([^│\s]+).*│?\s*$/iu.exec(line);
1775
+ if (!match) {
1776
+ return false;
1777
+ }
1778
+ return !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu
1779
+ .test(match[1]);
1780
+ });
1781
+ }
1782
+ function codexNativeInspectionComposerMismatchDiagnostic(screen, plan) {
1783
+ const expectedRows = CODEX_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile];
1784
+ if (!expectedRows) {
1785
+ return "composer_not_exact";
1786
+ }
1787
+ const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
1788
+ let composerIndex = -1;
1789
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
1790
+ if (CODEX_COMPOSER_MARKER.test(lines[index])) {
1791
+ composerIndex = index;
1792
+ break;
1793
+ }
1794
+ }
1795
+ if (composerIndex < 0 ||
1796
+ lines[composerIndex].replace(/^[›»]\s?/u, "").trimEnd() !== plan.command) {
1797
+ return "composer_not_exact";
1798
+ }
1799
+ const footerIndex = lines.findIndex((line, index) => index > composerIndex && CODEX_COMPOSER_FOOTER.test(line.trim()));
1800
+ const popupRows = lines.slice(composerIndex + 1, footerIndex < 0 ? lines.length : footerIndex).filter((line) => line.trim().length > 0);
1801
+ if (popupRows.length === 0) {
1802
+ return "composer_not_exact";
1803
+ }
1804
+ const logicalRows = [];
1805
+ let observedTruncation = false;
1806
+ for (const row of popupRows) {
1807
+ const trimmed = row.trim().replace(/\s+/gu, " ");
1808
+ if (trimmed.startsWith("/")) {
1809
+ logicalRows.push(trimmed);
1810
+ }
1811
+ else if (logicalRows.length > 0) {
1812
+ logicalRows[logicalRows.length - 1] += ` ${trimmed}`;
1813
+ observedTruncation = true;
1814
+ }
1815
+ else {
1816
+ return "composer_not_exact";
1817
+ }
1818
+ observedTruncation ||= trimmed.endsWith("…");
1819
+ }
1820
+ const normalizedExpected = expectedRows.map((row) => row.trim().replace(/\s+/gu, " "));
1821
+ const everyKnownPrefix = logicalRows.length <= normalizedExpected.length &&
1822
+ logicalRows.every((row, index) => {
1823
+ const withoutEllipsis = row.endsWith("…")
1824
+ ? row.slice(0, -1).trimEnd()
1825
+ : row;
1826
+ return normalizedExpected[index]?.startsWith(withoutEllipsis) === true;
1827
+ });
1828
+ return observedTruncation && everyKnownPrefix
1829
+ ? "composer_viewport_truncated"
1830
+ : "composer_not_exact";
1831
+ }
1454
1832
  function exactCodexComposerCapture(screen, expectedText) {
1455
1833
  const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
1456
1834
  const expectedComparable = composerComparableText(expectedText);
@@ -1471,7 +1849,7 @@ function exactCodexComposerCapture(screen, expectedText) {
1471
1849
  ...region.slice(1).map((line) => line.startsWith(" ") ? line.slice(2) : line)
1472
1850
  ];
1473
1851
  const comparable = composerComparableText(bodyRows.join("\n"));
1474
- const exactVisibleDraft = codexComposerRowsMatchExpected(bodyRows, expectedComparable);
1852
+ const exactVisibleDraft = terminalComposerRowsMatchExpected(bodyRows, expectedComparable);
1475
1853
  const exactLargePastePlaceholder = expectedCharacterCount > CODEX_LARGE_PASTE_CHAR_THRESHOLD &&
1476
1854
  comparable === largePasteComparable;
1477
1855
  if (!exactVisibleDraft && !exactLargePastePlaceholder) {
@@ -1489,9 +1867,10 @@ function exactCodexComposerCapture(screen, expectedText) {
1489
1867
  return matches[0];
1490
1868
  }
1491
1869
  /**
1492
- * Codex paints wrapped composer content as independent terminal rows, so tmux
1493
- * cannot distinguish a visual wrap from an authored newline. Align the rows
1494
- * against the exact text AKK injected instead of joining every row with `\n`.
1870
+ * Terminal UIs paint wrapped composer content as independent screen rows, so
1871
+ * a provider capture cannot distinguish a visual wrap from an authored
1872
+ * newline. Align the rows against the exact text AKK injected instead of
1873
+ * joining every row with `\n`.
1495
1874
  *
1496
1875
  * Only row boundaries are ambiguous: they may consume an authored newline, an
1497
1876
  * omitted run of ASCII spaces at a word wrap, or no character at a CJK/token
@@ -1499,7 +1878,7 @@ function exactCodexComposerCapture(screen, expectedText) {
1499
1878
  * empty row can only advance through an authored newline (plus terminal-trimmed
1500
1879
  * spaces before it), so blank-line structure is preserved.
1501
1880
  */
1502
- function codexComposerRowsMatchExpected(rows, expectedText) {
1881
+ function terminalComposerRowsMatchExpected(rows, expectedText) {
1503
1882
  const expected = composerComparableText(expectedText);
1504
1883
  if (rows.length === 0) {
1505
1884
  return expected.length === 0;
@@ -1570,11 +1949,11 @@ function exactClaudeComposerCapture(screen, expectedText) {
1570
1949
  if (region.length === 0 || !/^\s*❯(?:\s|$)/u.test(region[0])) {
1571
1950
  return undefined;
1572
1951
  }
1573
- const body = [
1952
+ const bodyRows = [
1574
1953
  region[0].replace(/^\s*❯\s?/u, ""),
1575
1954
  ...region.slice(1).map((line) => line.startsWith(" ") ? line.slice(2) : line)
1576
- ].join("\n");
1577
- if (composerComparableText(body) !== composerComparableText(expectedText)) {
1955
+ ];
1956
+ if (!terminalComposerRowsMatchExpected(bodyRows, expectedText)) {
1578
1957
  return undefined;
1579
1958
  }
1580
1959
  return {