@scotthuang/agent-knock-knock 0.12.3 → 0.12.5
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 +7 -5
- package/dist/src/cli-core.js +3422 -459
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/deferred-foreground-transfer.d.ts +112 -0
- package/dist/src/deferred-foreground-transfer.js +1063 -0
- package/dist/src/deferred-foreground-transfer.js.map +1 -0
- 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/session-store.js +7 -5
- package/dist/src/session-store.js.map +1 -1
- package/dist/src/store.d.ts +3 -1
- package/dist/src/store.js +24 -14
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +29 -0
- package/dist/src/terminal-agent-bridge.js +387 -16
- 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 +9 -6
|
@@ -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,
|
|
@@ -1458,6 +1673,162 @@ function claudeNativeInspectionTrailingIsFooter(lines) {
|
|
|
1458
1673
|
function nativeInspectionScreenFingerprint(screen) {
|
|
1459
1674
|
return `sha256:${createHash("sha256").update(screen).digest("hex")}`;
|
|
1460
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
|
+
}
|
|
1461
1832
|
function exactCodexComposerCapture(screen, expectedText) {
|
|
1462
1833
|
const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
|
|
1463
1834
|
const expectedComparable = composerComparableText(expectedText);
|