@scotthuang/agent-knock-knock 0.11.3 → 0.11.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 +22 -0
- package/README.md +15 -5
- package/dist/src/claude-terminal-agent-adapter.d.ts +13 -1
- package/dist/src/claude-terminal-agent-adapter.js +424 -0
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
- package/dist/src/cli.js +587 -12
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.d.ts +5 -1
- package/dist/src/codex-terminal-agent-adapter.js +360 -0
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin.js +50 -2
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +113 -0
- package/dist/src/terminal-agent-adapter.js +8 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +110 -1
- package/dist/src/terminal-agent-bridge.js +505 -0
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-control-provider.js +14 -1
- package/dist/src/terminal-control-provider.js.map +1 -1
- package/docs/quickstart-tmux.md +2 -0
- package/openclaw.plugin.json +5 -1
- package/package.json +8 -2
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +13 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExecutorKind } from "./executors.js";
|
|
2
|
-
import { type ActiveTerminalProcess, type TerminalAgentAdapter, type TerminalAgentAdapterCapabilities, type TerminalAgentAdapterRegistry, type TerminalCompletionEvidence, type TerminalControlRef, type TerminalDurableCompletionRequest, type TerminalProcessSnapshot, type TerminalRuntimeIdentity, type TerminalScreenInspection } from "./terminal-agent-adapter.js";
|
|
2
|
+
import { type ActiveTerminalProcess, type TerminalAgentAdapter, type TerminalAgentAdapterCapabilities, type TerminalAgentAdapterRegistry, type TerminalCompletionEvidence, type TerminalControlRef, type TerminalDurableCompletionRequest, type TerminalNativeInspectionEvidenceInventoryEntry, type TerminalNativeInspectionObservation, type TerminalNativeInspectionObservationRequest, type TerminalNativeInspectionPlan, type TerminalProcessSnapshot, type TerminalRuntimeIdentity, type TerminalScreenInspection } from "./terminal-agent-adapter.js";
|
|
3
3
|
import { type TerminalControlProvider } from "./terminal-control-provider.js";
|
|
4
4
|
/** A terminal send failed at a boundary that proves input never started. */
|
|
5
5
|
export declare class TerminalInputNotStartedError extends Error {
|
|
@@ -8,6 +8,30 @@ export declare class TerminalInputNotStartedError extends Error {
|
|
|
8
8
|
cause?: unknown;
|
|
9
9
|
});
|
|
10
10
|
}
|
|
11
|
+
export type NativeInspectionSubmissionStage = "not_started" | "text_injected" | "enter_uncertain";
|
|
12
|
+
/**
|
|
13
|
+
* A fail-closed native-inspection submission result.
|
|
14
|
+
*
|
|
15
|
+
* Only `not_started` proves that a caller may safely retry. Once text has
|
|
16
|
+
* reached the composer, or Enter has been attempted, automated retries could
|
|
17
|
+
* duplicate input or execute a different command after terminal drift.
|
|
18
|
+
*/
|
|
19
|
+
export declare class NativeInspectionSubmissionError extends Error {
|
|
20
|
+
readonly stage: NativeInspectionSubmissionStage;
|
|
21
|
+
readonly code = "AKK_NATIVE_INSPECTION_SUBMISSION_FAILED";
|
|
22
|
+
readonly doNotRetry: boolean;
|
|
23
|
+
constructor(stage: NativeInspectionSubmissionStage, message: string, options?: {
|
|
24
|
+
cause?: unknown;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** A verified modal could not be dismissed across one exact key attempt. */
|
|
28
|
+
export declare class NativeInspectionDismissalError extends Error {
|
|
29
|
+
readonly code = "AKK_NATIVE_INSPECTION_DISMISSAL_FAILED";
|
|
30
|
+
readonly doNotRetry = true;
|
|
31
|
+
constructor(message: string, options?: {
|
|
32
|
+
cause?: unknown;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
11
35
|
export interface TerminalBridgeStatus {
|
|
12
36
|
provider: "tmux";
|
|
13
37
|
target: string;
|
|
@@ -105,6 +129,65 @@ export interface TerminalSendResult {
|
|
|
105
129
|
terminalControl: TerminalControlRef;
|
|
106
130
|
multiline: boolean;
|
|
107
131
|
}
|
|
132
|
+
export type TerminalNativeInspectionMaterializationKind = "exact_slash_composer" | "exact_slash_popup";
|
|
133
|
+
export interface TerminalNativeInspectionMaterializationEvidence {
|
|
134
|
+
kind: TerminalNativeInspectionMaterializationKind;
|
|
135
|
+
digest: string;
|
|
136
|
+
stableForMs: number;
|
|
137
|
+
stableCaptures: number;
|
|
138
|
+
}
|
|
139
|
+
export interface TerminalNativeInspectionBeforeEnterContext {
|
|
140
|
+
agent: ExecutorKind;
|
|
141
|
+
terminalControl: TerminalControlRef;
|
|
142
|
+
plan: TerminalNativeInspectionPlan;
|
|
143
|
+
preEnterScreenDigest: string;
|
|
144
|
+
materialization: TerminalNativeInspectionMaterializationEvidence;
|
|
145
|
+
}
|
|
146
|
+
export interface TerminalNativeInspectionOptions {
|
|
147
|
+
runtime?: TerminalRuntimeIdentity;
|
|
148
|
+
/**
|
|
149
|
+
* Gives the CLI one final in-lock authorization point for its Store binding
|
|
150
|
+
* and action-token fences. The bridge recaptures the exact composer and
|
|
151
|
+
* revalidates terminal identity again after this hook before pressing Enter.
|
|
152
|
+
*/
|
|
153
|
+
beforeEnter?: (context: TerminalNativeInspectionBeforeEnterContext) => void | Promise<void>;
|
|
154
|
+
}
|
|
155
|
+
export interface TerminalNativeInspectionResult {
|
|
156
|
+
stage: "enter_dispatched";
|
|
157
|
+
agent: ExecutorKind;
|
|
158
|
+
terminalControl: TerminalControlRef;
|
|
159
|
+
command: string;
|
|
160
|
+
behaviorProfile: string;
|
|
161
|
+
preEnterScreenDigest: string;
|
|
162
|
+
preEnterEvidenceInventory: readonly TerminalNativeInspectionEvidenceInventoryEntry[];
|
|
163
|
+
materialization: TerminalNativeInspectionMaterializationEvidence;
|
|
164
|
+
enterCount: 1;
|
|
165
|
+
}
|
|
166
|
+
export interface TerminalNativeInspectionBeforeDismissContext {
|
|
167
|
+
agent: ExecutorKind;
|
|
168
|
+
terminalControl: TerminalControlRef;
|
|
169
|
+
plan: TerminalNativeInspectionPlan;
|
|
170
|
+
evidenceFingerprint: string;
|
|
171
|
+
}
|
|
172
|
+
export interface TerminalNativeInspectionDismissalOptions {
|
|
173
|
+
runtime?: TerminalRuntimeIdentity;
|
|
174
|
+
scrollbackLines?: number;
|
|
175
|
+
beforeDismiss?: (context: TerminalNativeInspectionBeforeDismissContext) => void | Promise<void>;
|
|
176
|
+
}
|
|
177
|
+
export interface TerminalNativeInspectionDismissalResult {
|
|
178
|
+
stage: "dismiss_dispatched";
|
|
179
|
+
agent: ExecutorKind;
|
|
180
|
+
terminalControl: TerminalControlRef;
|
|
181
|
+
keys: readonly string[];
|
|
182
|
+
dismissCount: 1;
|
|
183
|
+
}
|
|
184
|
+
export interface TerminalNativeInspectionObservationResult {
|
|
185
|
+
terminalControl: TerminalControlRef;
|
|
186
|
+
status: TerminalBridgeStatus;
|
|
187
|
+
/** Same raw-screen fingerprint format used by adapter stale checks. */
|
|
188
|
+
screenDigest: string;
|
|
189
|
+
observation: TerminalNativeInspectionObservation;
|
|
190
|
+
}
|
|
108
191
|
export interface TerminalApprovalAuthorizationContext {
|
|
109
192
|
agent: ExecutorKind;
|
|
110
193
|
terminalControl: TerminalControlRef;
|
|
@@ -152,6 +235,26 @@ export declare class TerminalAgentBridge {
|
|
|
152
235
|
runtime?: TerminalRuntimeIdentity;
|
|
153
236
|
}): Promise<TerminalBridgeStatus>;
|
|
154
237
|
send(agent: ExecutorKind, terminalControl: TerminalControlRef, text: string, options?: TerminalSendOptions): Promise<TerminalSendResult>;
|
|
238
|
+
/**
|
|
239
|
+
* Submit one closed, adapter-owned native inspection command.
|
|
240
|
+
*
|
|
241
|
+
* This intentionally does not use `send()`: native slash commands need a
|
|
242
|
+
* stricter composer proof, and failures after text injection must leave the
|
|
243
|
+
* draft untouched instead of issuing the legacy best-effort C-u cleanup.
|
|
244
|
+
*/
|
|
245
|
+
submitNativeInspection(agent: ExecutorKind, terminalControl: TerminalControlRef, plan: TerminalNativeInspectionPlan, options?: TerminalNativeInspectionOptions): Promise<TerminalNativeInspectionResult>;
|
|
246
|
+
observeNativeInspection(agent: ExecutorKind, terminalControl: TerminalControlRef, request: TerminalNativeInspectionObservationRequest, options?: {
|
|
247
|
+
runtime?: TerminalRuntimeIdentity;
|
|
248
|
+
scrollbackLines?: number;
|
|
249
|
+
}): Promise<TerminalNativeInspectionObservationResult>;
|
|
250
|
+
/**
|
|
251
|
+
* Dismiss one exact adapter-owned modal result after re-observing the same
|
|
252
|
+
* evidence under the terminal identity fence. There is exactly one key
|
|
253
|
+
* attempt and no automated retry across an uncertain dismissal boundary.
|
|
254
|
+
*/
|
|
255
|
+
dismissNativeInspection(agent: ExecutorKind, terminalControl: TerminalControlRef, plan: TerminalNativeInspectionPlan, request: TerminalNativeInspectionObservationRequest, expectedEvidenceFingerprint: string, options?: TerminalNativeInspectionDismissalOptions): Promise<TerminalNativeInspectionDismissalResult>;
|
|
256
|
+
private settleNativeInspectionComposer;
|
|
257
|
+
private revalidateNativeInspectionComposer;
|
|
155
258
|
private settleCodexMultilineComposer;
|
|
156
259
|
/**
|
|
157
260
|
* Prove that the exact managed draft is still present after Enter dispatch.
|
|
@@ -210,6 +313,12 @@ export declare class TerminalAgentBridge {
|
|
|
210
313
|
private captureInspection;
|
|
211
314
|
private verifyTerminalIdentity;
|
|
212
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Prove the exact idle input frame from the verified Claude Code 2.1.218
|
|
318
|
+
* profile. A loose or historical `❯` prompt is not enough to authorize even
|
|
319
|
+
* the initial literal `/status` injection.
|
|
320
|
+
*/
|
|
321
|
+
export declare function isExactClaudeNativeInspectionIdleComposer(screen: string): boolean;
|
|
213
322
|
export declare function terminalApprovalFingerprint(agent: ExecutorKind, terminalControl: TerminalControlRef, inspection: TerminalScreenInspection, options?: {
|
|
214
323
|
screen?: string;
|
|
215
324
|
runtime?: TerminalRuntimeIdentity;
|
|
@@ -11,6 +11,21 @@ const CODEX_MULTILINE_STABLE_CAPTURES = 2;
|
|
|
11
11
|
const CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES = 240;
|
|
12
12
|
const CODEX_COMPOSER_MARKER = /^[›»](?:\s|$)/u;
|
|
13
13
|
const CODEX_COMPOSER_FOOTER = /^(?:gpt-[\w.-]+(?:\s|$)|[-\w.]+ default ·)/u;
|
|
14
|
+
// Codex 0.146.0 and 0.146.1 render the same verified slash-completion row.
|
|
15
|
+
// Keep it closed per behavior profile so changed text cannot silently become
|
|
16
|
+
// a newly authorized native command surface.
|
|
17
|
+
const CODEX_NATIVE_STATUS_POPUP_BY_PROFILE = {
|
|
18
|
+
"codex-tui-0.146.0": " /status show current session configuration and token usage",
|
|
19
|
+
"codex-tui-0.146.1": " /status show current session configuration and token usage"
|
|
20
|
+
};
|
|
21
|
+
const CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE = {
|
|
22
|
+
"claude-code-2.1.218-native-status": [
|
|
23
|
+
"/status Show Claude Code status including version, model, account, API connectivity, and tool statuses",
|
|
24
|
+
"/statusline Set up Claude Code's status line UI",
|
|
25
|
+
"/ide Manage IDE integrations and show status",
|
|
26
|
+
"/usage Show session cost, plan usage, and activity stats"
|
|
27
|
+
]
|
|
28
|
+
};
|
|
14
29
|
const CODEX_LARGE_PASTE_CHAR_THRESHOLD = 1_000;
|
|
15
30
|
/** A terminal send failed at a boundary that proves input never started. */
|
|
16
31
|
export class TerminalInputNotStartedError extends Error {
|
|
@@ -20,6 +35,33 @@ export class TerminalInputNotStartedError extends Error {
|
|
|
20
35
|
this.name = "TerminalInputNotStartedError";
|
|
21
36
|
}
|
|
22
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* A fail-closed native-inspection submission result.
|
|
40
|
+
*
|
|
41
|
+
* Only `not_started` proves that a caller may safely retry. Once text has
|
|
42
|
+
* reached the composer, or Enter has been attempted, automated retries could
|
|
43
|
+
* duplicate input or execute a different command after terminal drift.
|
|
44
|
+
*/
|
|
45
|
+
export class NativeInspectionSubmissionError extends Error {
|
|
46
|
+
stage;
|
|
47
|
+
code = "AKK_NATIVE_INSPECTION_SUBMISSION_FAILED";
|
|
48
|
+
doNotRetry;
|
|
49
|
+
constructor(stage, message, options = {}) {
|
|
50
|
+
super(message, options);
|
|
51
|
+
this.stage = stage;
|
|
52
|
+
this.name = "NativeInspectionSubmissionError";
|
|
53
|
+
this.doNotRetry = stage !== "not_started";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** A verified modal could not be dismissed across one exact key attempt. */
|
|
57
|
+
export class NativeInspectionDismissalError extends Error {
|
|
58
|
+
code = "AKK_NATIVE_INSPECTION_DISMISSAL_FAILED";
|
|
59
|
+
doNotRetry = true;
|
|
60
|
+
constructor(message, options = {}) {
|
|
61
|
+
super(message, options);
|
|
62
|
+
this.name = "NativeInspectionDismissalError";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
23
65
|
export class TerminalAgentBridge {
|
|
24
66
|
registry;
|
|
25
67
|
terminalProvider;
|
|
@@ -219,6 +261,265 @@ export class TerminalAgentBridge {
|
|
|
219
261
|
multiline
|
|
220
262
|
};
|
|
221
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Submit one closed, adapter-owned native inspection command.
|
|
266
|
+
*
|
|
267
|
+
* This intentionally does not use `send()`: native slash commands need a
|
|
268
|
+
* stricter composer proof, and failures after text injection must leave the
|
|
269
|
+
* draft untouched instead of issuing the legacy best-effort C-u cleanup.
|
|
270
|
+
*/
|
|
271
|
+
async submitNativeInspection(agent, terminalControl, plan, options = {}) {
|
|
272
|
+
const adapter = this.registry.require(agent);
|
|
273
|
+
try {
|
|
274
|
+
assertClosedStatusInspectionPlan(adapter, terminalControl, plan);
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
throw nativeInspectionSubmissionError("not_started", error);
|
|
278
|
+
}
|
|
279
|
+
let verifiedForText;
|
|
280
|
+
try {
|
|
281
|
+
verifiedForText = await this.verifyTerminalIdentity(adapter.agent, terminalControl, options.runtime);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
throw nativeInspectionSubmissionError("not_started", error);
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
await this.terminalProvider.sendText(verifiedForText.target, plan.command, { socketPath: verifiedForText.socketPath });
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
if (error instanceof TerminalControlInputNotSentError) {
|
|
291
|
+
throw nativeInspectionSubmissionError("not_started", error);
|
|
292
|
+
}
|
|
293
|
+
// The transport cannot prove whether an untyped failure happened before
|
|
294
|
+
// or after tmux accepted the literal input. Fail closed as injected.
|
|
295
|
+
throw nativeInspectionSubmissionError("text_injected", error);
|
|
296
|
+
}
|
|
297
|
+
let settled;
|
|
298
|
+
try {
|
|
299
|
+
settled = await this.settleNativeInspectionComposer(adapter, terminalControl, plan, options.runtime);
|
|
300
|
+
await options.beforeEnter?.({
|
|
301
|
+
agent: adapter.agent,
|
|
302
|
+
terminalControl: settled.terminalControl,
|
|
303
|
+
plan,
|
|
304
|
+
preEnterScreenDigest: settled.screenDigest,
|
|
305
|
+
materialization: settled.materialization
|
|
306
|
+
});
|
|
307
|
+
settled = await this.revalidateNativeInspectionComposer(adapter, settled.terminalControl, plan, settled.materialization, options.runtime);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
throw nativeInspectionSubmissionError("text_injected", error);
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
// Exactly one Enter attempt. Any error is submission-uncertain and must
|
|
314
|
+
// never trigger a fallback executable or a blind second Enter.
|
|
315
|
+
await this.terminalProvider.sendKeys(settled.terminalControl.target, ["C-m"], { socketPath: settled.terminalControl.socketPath });
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
throw nativeInspectionSubmissionError("enter_uncertain", error);
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
stage: "enter_dispatched",
|
|
322
|
+
agent: adapter.agent,
|
|
323
|
+
terminalControl: settled.terminalControl,
|
|
324
|
+
command: plan.command,
|
|
325
|
+
behaviorProfile: plan.behaviorProfile,
|
|
326
|
+
preEnterScreenDigest: settled.screenDigest,
|
|
327
|
+
preEnterEvidenceInventory: settled.evidenceInventory,
|
|
328
|
+
materialization: settled.materialization,
|
|
329
|
+
enterCount: 1
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
async observeNativeInspection(agent, terminalControl, request, options = {}) {
|
|
333
|
+
const adapter = this.registry.require(agent);
|
|
334
|
+
if (!adapter.observeNativeInspection) {
|
|
335
|
+
throw new Error(`${adapter.displayName} native inspection observation is not supported`);
|
|
336
|
+
}
|
|
337
|
+
const captured = await this.captureInspection(adapter, terminalControl, {
|
|
338
|
+
runtime: options.runtime,
|
|
339
|
+
scrollbackLines: options.scrollbackLines
|
|
340
|
+
});
|
|
341
|
+
const screenDigest = nativeInspectionScreenFingerprint(captured.screen);
|
|
342
|
+
const adapterObservation = adapter.observeNativeInspection({
|
|
343
|
+
operation: request.operation,
|
|
344
|
+
previousScreenFingerprint: request.previousScreenFingerprint,
|
|
345
|
+
preEnterEvidenceInventory: request.preEnterEvidenceInventory,
|
|
346
|
+
expectedNativeThreadId: request.expectedNativeThreadId,
|
|
347
|
+
expectedAgentVersion: request.expectedAgentVersion,
|
|
348
|
+
expectedCwd: request.expectedCwd,
|
|
349
|
+
screen: captured.screen
|
|
350
|
+
});
|
|
351
|
+
const observation = adapter.agent === "claude" &&
|
|
352
|
+
options.runtime?.requireExactClaudeAgentRow === true &&
|
|
353
|
+
adapterObservation.status === "observed" &&
|
|
354
|
+
adapterObservation.evidence === "claude_status_panel"
|
|
355
|
+
? {
|
|
356
|
+
...adapterObservation,
|
|
357
|
+
evidence: "claude_status_panel+claude_agents_exact_pid"
|
|
358
|
+
}
|
|
359
|
+
: adapterObservation;
|
|
360
|
+
return {
|
|
361
|
+
terminalControl: captured.terminalControl,
|
|
362
|
+
status: statusFromInspection(adapter, captured.terminalControl, captured.inspection, { screen: captured.screen, runtime: options.runtime }),
|
|
363
|
+
screenDigest,
|
|
364
|
+
observation
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Dismiss one exact adapter-owned modal result after re-observing the same
|
|
369
|
+
* evidence under the terminal identity fence. There is exactly one key
|
|
370
|
+
* attempt and no automated retry across an uncertain dismissal boundary.
|
|
371
|
+
*/
|
|
372
|
+
async dismissNativeInspection(agent, terminalControl, plan, request, expectedEvidenceFingerprint, options = {}) {
|
|
373
|
+
const adapter = this.registry.require(agent);
|
|
374
|
+
try {
|
|
375
|
+
assertClosedStatusInspectionPlan(adapter, terminalControl, plan);
|
|
376
|
+
assertClosedNativeInspectionDismissal(plan);
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
throw new NativeInspectionDismissalError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
380
|
+
}
|
|
381
|
+
const observeExactPanel = async (control) => {
|
|
382
|
+
const captured = await this.captureInspection(adapter, control, {
|
|
383
|
+
runtime: options.runtime,
|
|
384
|
+
scrollbackLines: options.scrollbackLines
|
|
385
|
+
});
|
|
386
|
+
const observation = adapter.observeNativeInspection?.({
|
|
387
|
+
operation: request.operation,
|
|
388
|
+
previousScreenFingerprint: request.previousScreenFingerprint,
|
|
389
|
+
preEnterEvidenceInventory: request.preEnterEvidenceInventory,
|
|
390
|
+
expectedNativeThreadId: request.expectedNativeThreadId,
|
|
391
|
+
expectedAgentVersion: request.expectedAgentVersion,
|
|
392
|
+
expectedCwd: request.expectedCwd,
|
|
393
|
+
screen: captured.screen
|
|
394
|
+
});
|
|
395
|
+
if (!observation ||
|
|
396
|
+
observation.status !== "observed" ||
|
|
397
|
+
observation.result?.kind !== "native_status" ||
|
|
398
|
+
observation.evidenceFingerprint !== expectedEvidenceFingerprint) {
|
|
399
|
+
throw new Error(observation?.reason ??
|
|
400
|
+
"the exact fresh native status panel changed before dismissal");
|
|
401
|
+
}
|
|
402
|
+
const verified = await this.verifyTerminalIdentity(adapter.agent, captured.terminalControl, options.runtime);
|
|
403
|
+
if (!sameTerminalControlIdentity(captured.terminalControl, verified)) {
|
|
404
|
+
throw new Error("terminal control identity changed before native status dismissal");
|
|
405
|
+
}
|
|
406
|
+
return verified;
|
|
407
|
+
};
|
|
408
|
+
try {
|
|
409
|
+
let verified = await observeExactPanel(terminalControl);
|
|
410
|
+
await options.beforeDismiss?.({
|
|
411
|
+
agent: adapter.agent,
|
|
412
|
+
terminalControl: verified,
|
|
413
|
+
plan,
|
|
414
|
+
evidenceFingerprint: expectedEvidenceFingerprint
|
|
415
|
+
});
|
|
416
|
+
verified = await observeExactPanel(verified);
|
|
417
|
+
try {
|
|
418
|
+
await this.terminalProvider.sendKeys(verified.target, plan.expectedResult.dismissal.keys, { socketPath: verified.socketPath });
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
throw new NativeInspectionDismissalError("native status modal dismissal outcome is uncertain; do not retry automatically", { cause: error });
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
stage: "dismiss_dispatched",
|
|
425
|
+
agent: adapter.agent,
|
|
426
|
+
terminalControl: verified,
|
|
427
|
+
keys: plan.expectedResult.dismissal.keys,
|
|
428
|
+
dismissCount: 1
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
if (error instanceof NativeInspectionDismissalError) {
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
throw new NativeInspectionDismissalError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async settleNativeInspectionComposer(adapter, terminalControl, plan, runtime) {
|
|
439
|
+
const startedAt = performance.now();
|
|
440
|
+
let stableDigest;
|
|
441
|
+
let stableKind;
|
|
442
|
+
let stableSince;
|
|
443
|
+
let stableCaptures = 0;
|
|
444
|
+
while (performance.now() - startedAt <= CODEX_MULTILINE_SETTLE_TIMEOUT_MS) {
|
|
445
|
+
const captured = await this.captureInspection(adapter, terminalControl, {
|
|
446
|
+
runtime,
|
|
447
|
+
scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
|
|
448
|
+
});
|
|
449
|
+
assertNativeInspectionComposerSafe(captured.inspection);
|
|
450
|
+
const materialized = exactNativeInspectionComposerCapture(adapter.agent, captured.screen, plan);
|
|
451
|
+
const now = performance.now();
|
|
452
|
+
if (materialized) {
|
|
453
|
+
if (materialized.digest === stableDigest &&
|
|
454
|
+
materialized.kind === stableKind) {
|
|
455
|
+
stableCaptures += 1;
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
stableDigest = materialized.digest;
|
|
459
|
+
stableKind = materialized.kind;
|
|
460
|
+
stableSince = now;
|
|
461
|
+
stableCaptures = 1;
|
|
462
|
+
}
|
|
463
|
+
const stableForMs = stableSince === undefined ? 0 : now - stableSince;
|
|
464
|
+
if (stableCaptures >= CODEX_MULTILINE_STABLE_CAPTURES &&
|
|
465
|
+
stableForMs >= plan.composer.minimumStableMs) {
|
|
466
|
+
const evidence = {
|
|
467
|
+
kind: materialized.kind,
|
|
468
|
+
digest: materialized.digest,
|
|
469
|
+
stableForMs,
|
|
470
|
+
stableCaptures
|
|
471
|
+
};
|
|
472
|
+
return this.revalidateNativeInspectionComposer(adapter, captured.terminalControl, plan, evidence, runtime);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
stableDigest = undefined;
|
|
477
|
+
stableKind = undefined;
|
|
478
|
+
stableSince = undefined;
|
|
479
|
+
stableCaptures = 0;
|
|
480
|
+
}
|
|
481
|
+
const elapsed = performance.now() - startedAt;
|
|
482
|
+
const remaining = CODEX_MULTILINE_SETTLE_TIMEOUT_MS - elapsed;
|
|
483
|
+
if (remaining <= 0) {
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
await terminalSettleDelay(Math.min(CODEX_MULTILINE_SETTLE_POLL_MS, remaining));
|
|
487
|
+
}
|
|
488
|
+
throw new Error(`${adapter.displayName} /status composer did not become exact, idle, and stable before the bounded submit deadline`);
|
|
489
|
+
}
|
|
490
|
+
async revalidateNativeInspectionComposer(adapter, terminalControl, plan, expected, runtime) {
|
|
491
|
+
const captured = await this.captureInspection(adapter, terminalControl, {
|
|
492
|
+
runtime,
|
|
493
|
+
scrollbackLines: CODEX_MULTILINE_SETTLE_SCROLLBACK_LINES
|
|
494
|
+
});
|
|
495
|
+
assertNativeInspectionComposerSafe(captured.inspection);
|
|
496
|
+
const materialized = exactNativeInspectionComposerCapture(adapter.agent, captured.screen, plan);
|
|
497
|
+
if (!materialized ||
|
|
498
|
+
materialized.digest !== expected.digest ||
|
|
499
|
+
materialized.kind !== expected.kind) {
|
|
500
|
+
throw new Error(`${adapter.displayName} /status composer changed after its stable pre-submit capture`);
|
|
501
|
+
}
|
|
502
|
+
const baseline = adapter.observeNativeInspection?.({
|
|
503
|
+
operation: plan.operation,
|
|
504
|
+
screen: captured.screen
|
|
505
|
+
});
|
|
506
|
+
if (!baseline ||
|
|
507
|
+
baseline.status === "ambiguous" ||
|
|
508
|
+
!Array.isArray(baseline.evidenceInventory)) {
|
|
509
|
+
throw new Error(baseline?.reason ??
|
|
510
|
+
`${adapter.displayName} /status pre-Enter evidence inventory was not proven`);
|
|
511
|
+
}
|
|
512
|
+
const verifiedImmediatelyBeforeEnter = await this.verifyTerminalIdentity(adapter.agent, captured.terminalControl, runtime);
|
|
513
|
+
if (!sameTerminalControlIdentity(captured.terminalControl, verifiedImmediatelyBeforeEnter)) {
|
|
514
|
+
throw new Error(`terminal control identity changed after the final ${adapter.displayName} /status composer capture`);
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
terminalControl: verifiedImmediatelyBeforeEnter,
|
|
518
|
+
screenDigest: nativeInspectionScreenFingerprint(captured.screen),
|
|
519
|
+
evidenceInventory: baseline.evidenceInventory,
|
|
520
|
+
materialization: expected
|
|
521
|
+
};
|
|
522
|
+
}
|
|
222
523
|
async settleCodexMultilineComposer(adapter, terminalControl, expectedText, runtime) {
|
|
223
524
|
const startedAt = performance.now();
|
|
224
525
|
let stableDigest;
|
|
@@ -739,6 +1040,210 @@ export class TerminalAgentBridge {
|
|
|
739
1040
|
return result?.terminalControl ?? terminalControl;
|
|
740
1041
|
}
|
|
741
1042
|
}
|
|
1043
|
+
function nativeInspectionSubmissionError(stage, error) {
|
|
1044
|
+
if (error instanceof NativeInspectionSubmissionError) {
|
|
1045
|
+
const stageRank = {
|
|
1046
|
+
not_started: 0,
|
|
1047
|
+
text_injected: 1,
|
|
1048
|
+
enter_uncertain: 2
|
|
1049
|
+
};
|
|
1050
|
+
if (stageRank[error.stage] >= stageRank[stage]) {
|
|
1051
|
+
return error;
|
|
1052
|
+
}
|
|
1053
|
+
return new NativeInspectionSubmissionError(stage, error.message, {
|
|
1054
|
+
cause: error
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
return new NativeInspectionSubmissionError(stage, error instanceof Error ? error.message : String(error), { cause: error });
|
|
1058
|
+
}
|
|
1059
|
+
function assertClosedStatusInspectionPlan(adapter, terminalControl, plan) {
|
|
1060
|
+
if (!terminalControl.capabilities.includes("send_keys")) {
|
|
1061
|
+
throw new Error(`${adapter.displayName} terminal input is not supported`);
|
|
1062
|
+
}
|
|
1063
|
+
if (!terminalControl.capabilities.includes("screen_status")) {
|
|
1064
|
+
throw new Error(`${adapter.displayName} terminal screen inspection is not supported`);
|
|
1065
|
+
}
|
|
1066
|
+
const codexProfile = adapter.agent === "codex" && [
|
|
1067
|
+
"codex-tui-0.146.0",
|
|
1068
|
+
"codex-tui-0.146.1"
|
|
1069
|
+
].includes(plan.behaviorProfile);
|
|
1070
|
+
const claudeProfile = adapter.agent === "claude" &&
|
|
1071
|
+
plan.behaviorProfile === "claude-code-2.1.218-native-status";
|
|
1072
|
+
const exactPresentation = codexProfile
|
|
1073
|
+
? plan.expectedResult.presentation === "inline" &&
|
|
1074
|
+
plan.expectedResult.dismissal === undefined &&
|
|
1075
|
+
plan.composer.minimumStableMs >= CODEX_PASTE_ENTER_SETTLE_MS
|
|
1076
|
+
: claudeProfile
|
|
1077
|
+
? plan.expectedResult.presentation === "modal" &&
|
|
1078
|
+
plan.composer.minimumStableMs >= 80 &&
|
|
1079
|
+
plan.expectedResult.dismissal?.expected === "idle_empty_composer" &&
|
|
1080
|
+
JSON.stringify(plan.expectedResult.dismissal.keys) ===
|
|
1081
|
+
JSON.stringify(["Escape"])
|
|
1082
|
+
: false;
|
|
1083
|
+
if (plan.operation.kind !== "status" ||
|
|
1084
|
+
plan.command !== "/status" ||
|
|
1085
|
+
plan.effect !== "read_only" ||
|
|
1086
|
+
plan.requiresIdle !== true ||
|
|
1087
|
+
plan.composer.kind !== "exact" ||
|
|
1088
|
+
!Number.isFinite(plan.composer.minimumStableMs) ||
|
|
1089
|
+
plan.composer.minimumStableMs < 0 ||
|
|
1090
|
+
plan.expectedResult.kind !== "native_status" ||
|
|
1091
|
+
!exactPresentation) {
|
|
1092
|
+
throw new Error("refusing a non-closed or unverified native inspection plan");
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
function assertClosedNativeInspectionDismissal(plan) {
|
|
1096
|
+
if (plan.expectedResult.presentation !== "modal" ||
|
|
1097
|
+
plan.expectedResult.dismissal?.expected !== "idle_empty_composer" ||
|
|
1098
|
+
JSON.stringify(plan.expectedResult.dismissal.keys) !==
|
|
1099
|
+
JSON.stringify(["Escape"])) {
|
|
1100
|
+
throw new Error("native inspection has no closed verified modal dismissal plan");
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function assertNativeInspectionComposerSafe(inspection, displayName = "terminal agent") {
|
|
1104
|
+
if (inspection.approval.blocked ||
|
|
1105
|
+
inspection.activity.state === "awaiting_approval" ||
|
|
1106
|
+
inspection.activity.state === "working") {
|
|
1107
|
+
throw new Error(`${displayName} became busy or blocked while its /status composer was settling`);
|
|
1108
|
+
}
|
|
1109
|
+
// Codex's generic activity parser deliberately reports a non-empty slash
|
|
1110
|
+
// composer as unknown. At this stage the caller has already proved an idle,
|
|
1111
|
+
// empty styled composer under the terminal lock; the exact-current composer
|
|
1112
|
+
// capture below is the stronger continuation proof after AKK injected only
|
|
1113
|
+
// the adapter-owned /status command.
|
|
1114
|
+
}
|
|
1115
|
+
function exactNativeInspectionComposerCapture(agent, screen, plan) {
|
|
1116
|
+
return agent === "codex"
|
|
1117
|
+
? exactCodexNativeInspectionComposerCapture(screen, plan)
|
|
1118
|
+
: exactClaudeNativeInspectionComposerCapture(screen, plan);
|
|
1119
|
+
}
|
|
1120
|
+
function exactCodexNativeInspectionComposerCapture(screen, plan) {
|
|
1121
|
+
const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
|
|
1122
|
+
let currentComposerIndex = -1;
|
|
1123
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
1124
|
+
if (CODEX_COMPOSER_MARKER.test(lines[index])) {
|
|
1125
|
+
currentComposerIndex = index;
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
if (currentComposerIndex < 0) {
|
|
1130
|
+
return undefined;
|
|
1131
|
+
}
|
|
1132
|
+
const composerText = lines[currentComposerIndex]
|
|
1133
|
+
.replace(/^[›»]\s?/u, "")
|
|
1134
|
+
.trimEnd();
|
|
1135
|
+
if (composerText !== plan.command) {
|
|
1136
|
+
return undefined;
|
|
1137
|
+
}
|
|
1138
|
+
const footerIndex = lines.findIndex((line, candidateIndex) => candidateIndex > currentComposerIndex &&
|
|
1139
|
+
CODEX_COMPOSER_FOOTER.test(line.trim()));
|
|
1140
|
+
const region = lines.slice(currentComposerIndex, footerIndex < 0 ? lines.length : footerIndex);
|
|
1141
|
+
while (region.length > 1 && region.at(-1)?.trim() === "") {
|
|
1142
|
+
region.pop();
|
|
1143
|
+
}
|
|
1144
|
+
const popupRows = region.slice(1).filter((line) => line.trim().length > 0);
|
|
1145
|
+
let kind;
|
|
1146
|
+
if (popupRows.length === 0) {
|
|
1147
|
+
kind = "exact_slash_composer";
|
|
1148
|
+
}
|
|
1149
|
+
else if (popupRows.length === 1 &&
|
|
1150
|
+
popupRows[0].trimEnd() ===
|
|
1151
|
+
CODEX_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile]) {
|
|
1152
|
+
kind = "exact_slash_popup";
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
return undefined;
|
|
1156
|
+
}
|
|
1157
|
+
return {
|
|
1158
|
+
kind,
|
|
1159
|
+
digest: createHash("sha256").update(region.join("\n")).digest("hex")
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
function exactClaudeNativeInspectionComposerCapture(screen, plan) {
|
|
1163
|
+
const frame = exactClaudeNativeInspectionComposerFrame(screen);
|
|
1164
|
+
if (!frame) {
|
|
1165
|
+
return undefined;
|
|
1166
|
+
}
|
|
1167
|
+
const { lines, openIndex, closeIndex, composerRows, trailing } = frame;
|
|
1168
|
+
if (composerRows.length !== 1 ||
|
|
1169
|
+
!/^\s*❯(?:\s|$)/u.test(composerRows[0]) ||
|
|
1170
|
+
composerRows[0].replace(/^\s*❯\s?/u, "").trimEnd() !== plan.command) {
|
|
1171
|
+
return undefined;
|
|
1172
|
+
}
|
|
1173
|
+
let kind;
|
|
1174
|
+
if (trailing.length === 0 || claudeNativeInspectionTrailingIsFooter(trailing)) {
|
|
1175
|
+
kind = "exact_slash_composer";
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
const suggestions = [];
|
|
1179
|
+
for (const line of trailing) {
|
|
1180
|
+
const trimmed = line.trim();
|
|
1181
|
+
if (trimmed.startsWith("/")) {
|
|
1182
|
+
suggestions.push(trimmed.replace(/\s+/gu, " "));
|
|
1183
|
+
}
|
|
1184
|
+
else if (suggestions.length > 0) {
|
|
1185
|
+
suggestions[suggestions.length - 1] +=
|
|
1186
|
+
` ${trimmed.replace(/\s+/gu, " ")}`;
|
|
1187
|
+
}
|
|
1188
|
+
else {
|
|
1189
|
+
return undefined;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
if (JSON.stringify(suggestions) !== JSON.stringify(CLAUDE_NATIVE_STATUS_POPUP_BY_PROFILE[plan.behaviorProfile])) {
|
|
1193
|
+
return undefined;
|
|
1194
|
+
}
|
|
1195
|
+
kind = "exact_slash_popup";
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
kind,
|
|
1199
|
+
digest: createHash("sha256")
|
|
1200
|
+
.update(lines.slice(openIndex, closeIndex + 1).concat(trailing).join("\n"))
|
|
1201
|
+
.digest("hex")
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Prove the exact idle input frame from the verified Claude Code 2.1.218
|
|
1206
|
+
* profile. A loose or historical `❯` prompt is not enough to authorize even
|
|
1207
|
+
* the initial literal `/status` injection.
|
|
1208
|
+
*/
|
|
1209
|
+
export function isExactClaudeNativeInspectionIdleComposer(screen) {
|
|
1210
|
+
const frame = exactClaudeNativeInspectionComposerFrame(screen);
|
|
1211
|
+
if (!frame) {
|
|
1212
|
+
return false;
|
|
1213
|
+
}
|
|
1214
|
+
return (frame.composerRows.length === 1 &&
|
|
1215
|
+
/^\s*❯\s*$/u.test(frame.composerRows[0]) &&
|
|
1216
|
+
(frame.trailing.length === 0 ||
|
|
1217
|
+
claudeNativeInspectionTrailingIsFooter(frame.trailing)));
|
|
1218
|
+
}
|
|
1219
|
+
function exactClaudeNativeInspectionComposerFrame(screen) {
|
|
1220
|
+
const lines = screen.replace(/\r\n?/gu, "\n").replace(/\u00a0/gu, " ")
|
|
1221
|
+
.split("\n");
|
|
1222
|
+
const dividerIndexes = lines
|
|
1223
|
+
.map((line, index) => /^\s*[─━]{8,}\s*$/u.test(line) ? index : -1)
|
|
1224
|
+
.filter((index) => index >= 0);
|
|
1225
|
+
if (dividerIndexes.length < 2) {
|
|
1226
|
+
return undefined;
|
|
1227
|
+
}
|
|
1228
|
+
const closeIndex = dividerIndexes.at(-1);
|
|
1229
|
+
const openIndex = dividerIndexes.at(-2);
|
|
1230
|
+
return {
|
|
1231
|
+
lines,
|
|
1232
|
+
openIndex,
|
|
1233
|
+
closeIndex,
|
|
1234
|
+
composerRows: lines.slice(openIndex + 1, closeIndex)
|
|
1235
|
+
.filter((line) => line.trim().length > 0),
|
|
1236
|
+
trailing: lines.slice(closeIndex + 1)
|
|
1237
|
+
.filter((line) => line.trim().length > 0)
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
function claudeNativeInspectionTrailingIsFooter(lines) {
|
|
1241
|
+
return lines.length <= 2 && lines.every((line) => /^\s*(?:[⏵⏴]{1,2}|\?)\s*.*(?:shift\+tab|accept edits|bypass permissions|for shortcuts|← for agents)/iu
|
|
1242
|
+
.test(line));
|
|
1243
|
+
}
|
|
1244
|
+
function nativeInspectionScreenFingerprint(screen) {
|
|
1245
|
+
return `sha256:${createHash("sha256").update(screen).digest("hex")}`;
|
|
1246
|
+
}
|
|
742
1247
|
function exactCodexComposerCapture(screen, expectedText) {
|
|
743
1248
|
const lines = screen.replace(/\r\n?/gu, "\n").split("\n");
|
|
744
1249
|
const expectedComparable = composerComparableText(expectedText);
|