@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.
- package/CHANGELOG.md +25 -0
- package/README.md +4 -4
- package/dist/src/cli-core.js +1105 -176
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/herdr-terminal-control-provider.d.ts +26 -1
- package/dist/src/herdr-terminal-control-provider.js +325 -4
- package/dist/src/herdr-terminal-control-provider.js.map +1 -1
- package/dist/src/openclaw-plugin.js +2 -2
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +37 -3
- package/dist/src/terminal-agent-bridge.js +410 -31
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-control-provider.d.ts +19 -0
- package/dist/src/terminal-control-provider.js +86 -7
- package/dist/src/terminal-control-provider.js.map +1 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +3 -3
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
1413
|
-
*
|
|
1414
|
-
*
|
|
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
|
|
1417
|
-
const frame =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
*
|
|
1493
|
-
* cannot distinguish a visual wrap from an authored
|
|
1494
|
-
* against the exact text AKK injected instead of
|
|
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
|
|
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
|
|
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
|
-
]
|
|
1577
|
-
if (
|
|
1955
|
+
];
|
|
1956
|
+
if (!terminalComposerRowsMatchExpected(bodyRows, expectedText)) {
|
|
1578
1957
|
return undefined;
|
|
1579
1958
|
}
|
|
1580
1959
|
return {
|