@scotthuang/agent-knock-knock 0.11.4 → 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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.5 - 2026-08-09
4
+
5
+ ### Added
6
+
7
+ - Extend the closed `native_inspect(status)` action to exact Claude Code 2.1.218 with an adapter-owned `/status` plan, a separately measured 80 ms composer-stability boundary, bounded and redacted Status-panel parsing, and safe return to the original idle composer.
8
+
9
+ ### Security
10
+
11
+ - Fence Claude inspection with a fresh snapshot token, the shared terminal lock, exact pane, PID, process birth, cwd, binding, unique `claude agents --json --all` Session identity, and exclusive active ownership before and across terminal input.
12
+ - Keep ordinary `send` and `respond` slash-command rejection intact, issue at most one Enter and one Escape without blind retries, create no AKK lifecycle state, and leave `/usage`, `/cost`, `/stats`, and `/usage-credits` unavailable.
13
+
3
14
  ## 0.11.4 - 2026-08-09
4
15
 
5
16
  ### Added
package/README.md CHANGED
@@ -87,7 +87,7 @@ Human-friendly selectors such as `only`, `codex`, `claude`, a terminal ID, or `@
87
87
 
88
88
  Native clear/new/resume operations are explicit lifecycle actions, separate from ordinary Turn creation. A successful new/clear creates a new native thread and AKK Session; resume activates the exact historical native thread and its corresponding Session. Each successful lifecycle transition creates no Turn. The next ordinary send creates the first Turn in the selected context. AKK serializes the transition, verifies the resulting native identity, and advances the terminal binding generation so work and callbacks from the previous context cannot cross the boundary.
89
89
 
90
- AKK status and native status inspection are different operations. `agent_knock_knock_status` (and `/akk status`) reads AKK Turn state plus a bounded current terminal screen; it does not execute the coding agent's `/status`. When an idle terminal row advertises `native_inspect`, `agent_knock_knock_native_inspect` can execute only its prefilled, version-scoped inspection with the exact `terminal_id`, `inspection="status"`, and fresh `expected_binding_token`. Initial support is Codex-only and limited to versions 0.146.0 and 0.146.1. It creates no Session, Turn, receipt, monitor, or callback. Claude native commands, `/usage`, `/model`, `/compact`, arbitrary slash strings, and unsupported Codex versions remain unavailable. In particular, bare Codex `/usage` opens an interactive menu whose later Enter can select an account-side usage-limit reset; do not automate it as a read-only inspection.
90
+ AKK status and native status inspection are different operations. `agent_knock_knock_status` (and `/akk status`) reads AKK Turn state plus a bounded current terminal screen; it does not execute the coding agent's `/status`. When an idle terminal row advertises `native_inspect`, `agent_knock_knock_native_inspect` can execute only its prefilled, version-scoped inspection with the exact `terminal_id`, `inspection="status"`, and fresh `expected_binding_token`. Supported profiles are Codex 0.146.0/0.146.1 and Claude Code 2.1.218. Claude's adapter parses a newly opened Status panel, dismisses that exact panel once, and proves the same pane returned to an idle empty composer. The action creates no Session, Turn, receipt, monitor, or callback. `/usage`, `/cost`, `/stats`, `/usage-credits`, `/model`, `/compact`, arbitrary slash strings, and unsupported versions remain unavailable. In particular, bare Codex `/usage` opens an interactive menu whose later Enter can select an account-side usage-limit reset; do not automate it as a read-only inspection.
91
91
 
92
92
  ## Optional: Natural-Language Delegation
93
93
 
@@ -134,7 +134,7 @@ If you also want standalone shell commands such as `agent-knock-knock doctor`, i
134
134
  npm install -g @scotthuang/agent-knock-knock
135
135
  ```
136
136
 
137
- Standalone `agent-knock-knock list` and AKK `status` are read-only with respect to managed-turn state by default. Passing `--reconcile` explicitly enables controlled reconciliation; OpenClaw does this for `/akk list`, `/akk status`, and the corresponding plugin tools. This AKK status path does not run native Codex `/status`; the separately advertised `native_inspect` action owns that bounded terminal input.
137
+ Standalone `agent-knock-knock list` and AKK `status` are read-only with respect to managed-turn state by default. Passing `--reconcile` explicitly enables controlled reconciliation; OpenClaw does this for `/akk list`, `/akk status`, and the corresponding plugin tools. This AKK status path does not run the coding agent's native `/status`; the separately advertised `native_inspect` action owns that bounded terminal input.
138
138
 
139
139
  ### Alternative: Install from npm
140
140
 
@@ -228,7 +228,7 @@ Selectors fail closed: `only` works only with one actionable target, `latest` re
228
228
 
229
229
  To change native context, first copy the full `terminal_id` from `/akk list`; lifecycle commands do not accept an ordinary-send `@short-ref` or loose agent selector. `/akk threads <exact-terminal-id>` lists exact, same-workspace candidates with a deterministic number, a collision-safe display-only `@short-id`, an opaque snapshot handle, and the complete UUID. `/akk resume-thread <exact-terminal-id>` without a selection shows that list. A complete UUID remains compatible. A number or short ID resolves only against the latest list displayed in the same OpenClaw session, while an opaque handle names its exact snapshot; all expire after five minutes and fail after terminal, process, workspace, binding, candidate-set, or relevant action changes. None is ever passed to Codex or Claude Code as native identity: AKK resolves the saved tuple back to its full UUID and fresh evidence tokens first. `previous` (or `刚才那个`) is advertised only when the current Session's latest committed lifecycle transition identifies exactly one currently verified resumable source; it never guesses from title, recency, or static lineage. `/akk new-thread` and its human alias `/akk clear-thread` start a clean context. AKK does not poll bindings in the background: if a recorded owner process exits, the next lifecycle listing can classify that sole historical binding as resumable, and the resume mutation compare-and-swap detaches it before touching the terminal. Live, stale, expired, unsupported, busy, ambiguous, active-elsewhere, or unverifiable transitions fail closed. Do not send `/clear`, `/new`, `/resume`, `/status`, Codex `/fork`, `/side`, or `/btw`, Claude `/branch`, or any other first-line native slash command as an ordinary task or answer; use an advertised AKK action, express the request in natural language, or enter an unsupported native command manually in tmux.
230
230
 
231
- To request the native Codex status card, first run `agent_knock_knock_list` and use only that terminal row's advertised `native_inspect` action. The structured tool schema is closed to `inspection="status"`; callers cannot provide `/status` or another slash command as text. AKK serializes the inspection with terminal mutations, revalidates the fresh token and exact terminal identity, and returns only after it proves one fresh bounded status result and an idle postcondition. It never turns ordinary `send` or `respond` into a slash-command escape hatch.
231
+ To request a native Codex status card or Claude Status panel, first run `agent_knock_knock_list` and use only that terminal row's advertised `native_inspect` action. The structured tool schema is closed to `inspection="status"`; callers cannot provide `/status` or another slash command as text. AKK serializes the inspection with terminal mutations, revalidates the fresh token and exact terminal identity, and returns only after it proves one fresh bounded status result and an idle postcondition. For Claude, that includes safely dismissing the exact modal once. It never turns ordinary `send` or `respond` into a slash-command escape hatch.
232
232
 
233
233
  The top-level v7 `action_contracts` documents this closed inspection alongside the existing lifecycle and Turn actions; `available_actions` remains the only current-action authority.
234
234
 
@@ -236,7 +236,7 @@ For natural-language tool use, `agent_knock_knock_list` is terminal-first. Each
236
236
 
237
237
  Use only an `available_actions` entry returned in that snapshot, begin with its prefilled authoritative arguments, and supply every `missing_required` field. A managed Session's `send` uses its prefilled `session_id` and creates a new Turn. For first attach only, use a discovery selector explicitly named by the user or the unmanaged raw-terminal row's prefilled `selector`; do not infer or reuse one. `respond` is available only while a Turn is `waiting_for_openclaw`; it uses `turn_id` and keeps the answer inside that Turn. Managed status, approval, cancellation, renewal, callback retry, and close also use the exact `turn_id`. Native inspection instead uses the exact terminal row's `terminal_id`, closed `inspection`, and snapshot-bound `expected_binding_token`; do not substitute AKK status or ordinary send. A raw terminal may be controlled only through the exact status, approval, cancellation, or orphan-close action that its own row advertises with a prefilled `conversation_id`; never construct or guess one. For an ordinary send, add only `request`—`timeoutSeconds` is unsupported, and monitoring limits should be omitted unless the user explicitly asks to change them. AKK revalidates availability before every side effect.
238
238
 
239
- The top-level action contracts include `native_inspect`, `list_resumable_threads`, `new_thread`, `resume_thread`, and the conflict-only `reconcile_binding` recovery action. A supported idle Codex terminal may advertise `native_inspect` with its exact terminal ID, the closed `status` inspection kind, and a fresh binding token. The terminal row also advertises `list_resumable_threads` and, when currently safe, `new_thread`. Thread listing is read-only with respect to Session/Turn state, takes only the full `terminal_id`, and returns a fresh `expected_binding_token` plus candidate rows; each `resumable=true` row retains its complete UUID and exact prefilled `resume_thread` action. If `previous` is present, use only its exact prefilled action for a natural-language “刚才那个” request. Numbers, short IDs, and handles are human display/navigation aids, never tool arguments or authoritative native identity. The `new_thread` and `resume_thread` mutations require the fresh token, and resume additionally requires the candidate's complete `native_thread_id` and opaque `candidate_token`. A binding conflict may advertise `reconcile_binding` only when one exact Session claim can be safely CAS-detached using its listed revision, binding token, and live terminal token; this never adopts the replacement thread, sends terminal input, or creates a Turn. Never construct, guess, truncate, combine across snapshots, or reuse those values after another terminal action. Native inspection and lifecycle results contain no `turn_id` because no work was sent.
239
+ The top-level action contracts include `native_inspect`, `list_resumable_threads`, `new_thread`, `resume_thread`, and the conflict-only `reconcile_binding` recovery action. A supported idle Codex or Claude Code terminal may advertise `native_inspect` with its exact terminal ID, the closed `status` inspection kind, and a fresh binding token. The terminal row also advertises `list_resumable_threads` and, when currently safe, `new_thread`. Thread listing is read-only with respect to Session/Turn state, takes only the full `terminal_id`, and returns a fresh `expected_binding_token` plus candidate rows; each `resumable=true` row retains its complete UUID and exact prefilled `resume_thread` action. If `previous` is present, use only its exact prefilled action for a natural-language “刚才那个” request. Numbers, short IDs, and handles are human display/navigation aids, never tool arguments or authoritative native identity. The `new_thread` and `resume_thread` mutations require the fresh token, and resume additionally requires the candidate's complete `native_thread_id` and opaque `candidate_token`. A binding conflict may advertise `reconcile_binding` only when one exact Session claim can be safely CAS-detached using its listed revision, binding token, and live terminal token; this never adopts the replacement thread, sends terminal input, or creates a Turn. Never construct, guess, truncate, combine across snapshots, or reuse those values after another terminal action. Native inspection and lifecycle results contain no `turn_id` because no work was sent.
240
240
 
241
241
  Workspace is not a routing boundary. AKK can list, inspect, and control verified panes across projects; when more than one target matches, use a selector to choose one explicitly.
242
242
 
@@ -1,4 +1,4 @@
1
- import type { ActiveTerminalProcess, TerminalAgentAdapter, TerminalApprovalInspection, TerminalDurableCompletionRequest, TerminalProcessSnapshot, TerminalScreenInspection, TerminalScreenInspectionOptions, TerminalThreadLifecycleCapabilities, TerminalThreadLifecycleObservation, TerminalThreadLifecycleObservationRequest, TerminalThreadLifecycleOperation, TerminalThreadLifecyclePlan } from "./terminal-agent-adapter.js";
1
+ import type { ActiveTerminalProcess, TerminalAgentAdapter, TerminalApprovalInspection, TerminalDurableCompletionRequest, TerminalNativeInspectionCapabilities, TerminalNativeInspectionObservation, TerminalNativeInspectionObservationRequest, TerminalNativeInspectionOperation, TerminalNativeInspectionPlan, TerminalProcessSnapshot, TerminalScreenInspection, TerminalScreenInspectionOptions, TerminalThreadLifecycleCapabilities, TerminalThreadLifecycleObservation, TerminalThreadLifecycleObservationRequest, TerminalThreadLifecycleOperation, TerminalThreadLifecyclePlan } from "./terminal-agent-adapter.js";
2
2
  export type ClaudeProcessKind = "claude_cli";
3
3
  /** A row returned by `claude agents --json --all`. Unknown fields are intentionally ignored. */
4
4
  export interface ClaudeAgentRow {
@@ -8,6 +8,7 @@ export interface ClaudeAgentRow {
8
8
  sessionId?: string;
9
9
  startedAt?: number;
10
10
  status?: string;
11
+ waitingFor?: string;
11
12
  }
12
13
  export interface CreateClaudeTerminalAgentAdapterOptions {
13
14
  /**
@@ -39,7 +40,18 @@ export interface ClaudePendingApprovalEvidence {
39
40
  evidenceFingerprint: string;
40
41
  observedEndOffsetBytes: number;
41
42
  }
43
+ /**
44
+ * On the verified 2.1.218 TUI, the exact `/status` composer and its closed
45
+ * completion list remained byte-stable across an 80 ms capture interval and
46
+ * one subsequent Enter opened the Status panel. This is a Claude-local
47
+ * single-line profile; it deliberately does not reuse Codex's 121 ms burst
48
+ * paste boundary.
49
+ */
50
+ export declare const CLAUDE_NATIVE_INSPECTION_COMPOSER_STABLE_MS = 80;
42
51
  export declare function createClaudeTerminalAgentAdapter(options?: CreateClaudeTerminalAgentAdapterOptions): TerminalAgentAdapter<ClaudeProcessKind>;
52
+ export declare function probeClaudeNativeInspection(agentVersion: string | undefined): TerminalNativeInspectionCapabilities;
53
+ export declare function planClaudeNativeInspection(operation: TerminalNativeInspectionOperation, capabilities: TerminalNativeInspectionCapabilities): TerminalNativeInspectionPlan;
54
+ export declare function observeClaudeNativeInspection(request: TerminalNativeInspectionObservationRequest): TerminalNativeInspectionObservation;
43
55
  export declare function probeClaudeThreadLifecycle(agentVersion: string | undefined): TerminalThreadLifecycleCapabilities;
44
56
  export declare function planClaudeThreadLifecycle(operation: TerminalThreadLifecycleOperation, capabilities: TerminalThreadLifecycleCapabilities): TerminalThreadLifecyclePlan;
45
57
  export declare function observeClaudeThreadLifecycle(request: TerminalThreadLifecycleObservationRequest, legacyOperation?: undefined, fallbackAgentRows?: readonly ClaudeAgentRow[]): TerminalThreadLifecycleObservation;
@@ -75,6 +75,43 @@ const CLAUDE_AUTO_APPROVAL_COMMAND_LENGTH = 2000;
75
75
  const CLAUDE_NATIVE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/u;
76
76
  const CLAUDE_LIFECYCLE_VERSION = "2.1.218";
77
77
  const CLAUDE_LIFECYCLE_PROFILE = "claude-code-2.1.218";
78
+ const CLAUDE_NATIVE_INSPECTION_VERSION = "2.1.218";
79
+ const CLAUDE_NATIVE_INSPECTION_PROFILE = "claude-code-2.1.218-native-status";
80
+ /**
81
+ * On the verified 2.1.218 TUI, the exact `/status` composer and its closed
82
+ * completion list remained byte-stable across an 80 ms capture interval and
83
+ * one subsequent Enter opened the Status panel. This is a Claude-local
84
+ * single-line profile; it deliberately does not reuse Codex's 121 ms burst
85
+ * paste boundary.
86
+ */
87
+ export const CLAUDE_NATIVE_INSPECTION_COMPOSER_STABLE_MS = 80;
88
+ const CLAUDE_STATUS_PANEL_HEADER = /^\s*Settings\s+Status\s+Config\s+Usage\s+Stats\s*$/u;
89
+ const CLAUDE_STATUS_PANEL_FOOTER = /^\s*Esc to cancel\s*$/u;
90
+ const CLAUDE_STATUS_PANEL_DIVIDER = /^\s*[─━]{8,}\s*$/u;
91
+ const CLAUDE_STATUS_PANEL_FIELD = /^\s{0,4}([^:]{1,64}):\s+(.{1,1024})\s*$/u;
92
+ const CLAUDE_STATUS_PANEL_MAX_SCAN_LINES = 512;
93
+ const CLAUDE_STATUS_PANEL_MAX_LINES = 48;
94
+ const CLAUDE_STATUS_PANEL_MAX_REGION_LENGTH = 8_192;
95
+ const CLAUDE_STATUS_PANEL_MAX_FIELDS = 24;
96
+ const CLAUDE_STATUS_PANEL_MAX_FIELD_VALUE_LENGTH = 512;
97
+ const CLAUDE_STATUS_PANEL_MAX_EXCERPT_LENGTH = 4_000;
98
+ const CLAUDE_STATUS_PANEL_MAX_EVIDENCE_ENTRIES = 24;
99
+ const CLAUDE_STATUS_PANEL_FIELDS = new Set([
100
+ "Version",
101
+ "Session name",
102
+ "Session ID",
103
+ "cwd",
104
+ "Auth token",
105
+ "Anthropic base URL",
106
+ "Model",
107
+ "MCP servers",
108
+ "Setting sources"
109
+ ]);
110
+ const CLAUDE_STATUS_PANEL_SENSITIVE_FIELDS = new Set([
111
+ "Session name",
112
+ "Auth token"
113
+ ]);
114
+ const NATIVE_INSPECTION_FINGERPRINT = /^sha256:[0-9a-f]{64}$/u;
78
115
  const ANSI_ESCAPE_PATTERN = /\x1B(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/gu;
79
116
  export function createClaudeTerminalAgentAdapter(options = {}) {
80
117
  const agentRows = options.agentRows ?? [];
@@ -106,6 +143,9 @@ export function createClaudeTerminalAgentAdapter(options = {}) {
106
143
  probeThreadLifecycle: probeClaudeThreadLifecycle,
107
144
  planThreadLifecycle: planClaudeThreadLifecycle,
108
145
  observeThreadLifecycle: lifecycleObserver,
146
+ probeNativeInspection: probeClaudeNativeInspection,
147
+ planNativeInspection: planClaudeNativeInspection,
148
+ observeNativeInspection: observeClaudeNativeInspection,
109
149
  ...(durableCompletion
110
150
  ? {
111
151
  async detectDurableCompletion(request) {
@@ -115,6 +155,191 @@ export function createClaudeTerminalAgentAdapter(options = {}) {
115
155
  : {})
116
156
  };
117
157
  }
158
+ export function probeClaudeNativeInspection(agentVersion) {
159
+ if (!agentVersion) {
160
+ return {
161
+ status: "unknown",
162
+ statusInspection: false,
163
+ reason: "the running Claude Code version could not be verified"
164
+ };
165
+ }
166
+ const supported = agentVersion === CLAUDE_NATIVE_INSPECTION_VERSION;
167
+ return {
168
+ status: supported ? "supported" : "unsupported",
169
+ agentVersion,
170
+ behaviorProfile: supported
171
+ ? CLAUDE_NATIVE_INSPECTION_PROFILE
172
+ : undefined,
173
+ statusInspection: supported,
174
+ reason: supported
175
+ ? "Claude Code /status native inspection is supported by the verified version"
176
+ : "this exact Claude Code version has no AKK native inspection behavior profile"
177
+ };
178
+ }
179
+ export function planClaudeNativeInspection(operation, capabilities) {
180
+ if (operation.kind !== "status" ||
181
+ capabilities.status !== "supported" ||
182
+ capabilities.statusInspection !== true ||
183
+ capabilities.agentVersion !== CLAUDE_NATIVE_INSPECTION_VERSION ||
184
+ capabilities.behaviorProfile !== CLAUDE_NATIVE_INSPECTION_PROFILE) {
185
+ throw new Error(capabilities.reason);
186
+ }
187
+ return {
188
+ operation,
189
+ behaviorProfile: CLAUDE_NATIVE_INSPECTION_PROFILE,
190
+ command: "/status",
191
+ effect: "read_only",
192
+ requiresIdle: true,
193
+ composer: {
194
+ kind: "exact",
195
+ minimumStableMs: CLAUDE_NATIVE_INSPECTION_COMPOSER_STABLE_MS
196
+ },
197
+ expectedResult: {
198
+ kind: "native_status",
199
+ presentation: "modal",
200
+ dismissal: {
201
+ keys: ["Escape"],
202
+ expected: "idle_empty_composer"
203
+ }
204
+ }
205
+ };
206
+ }
207
+ export function observeClaudeNativeInspection(request) {
208
+ const rawScreen = request.screen ?? "";
209
+ const rawScreenFingerprint = fingerprintClaudeNativeInspection(rawScreen);
210
+ const screen = normalizeClaudeNativeInspectionScreen(rawScreen);
211
+ const screenFingerprint = fingerprintClaudeNativeInspection(screen);
212
+ const inventory = claudeStatusPanelEvidenceInventory(screen);
213
+ if (inventory.status === "ambiguous") {
214
+ return {
215
+ status: "ambiguous",
216
+ screenFingerprint,
217
+ evidenceInventory: inventory.entries,
218
+ reason: inventory.reason
219
+ };
220
+ }
221
+ const baselineError = validateClaudeStatusEvidenceInventory(request.preEnterEvidenceInventory);
222
+ if (baselineError) {
223
+ return {
224
+ status: "ambiguous",
225
+ screenFingerprint,
226
+ evidenceInventory: inventory.entries,
227
+ reason: baselineError
228
+ };
229
+ }
230
+ if (request.previousScreenFingerprint !== undefined &&
231
+ (request.previousScreenFingerprint === rawScreenFingerprint ||
232
+ request.previousScreenFingerprint === screenFingerprint)) {
233
+ return {
234
+ status: "stale",
235
+ screenFingerprint,
236
+ evidenceInventory: inventory.entries,
237
+ reason: "the Claude screen did not change after the native inspection command"
238
+ };
239
+ }
240
+ const parsed = parseCurrentClaudeStatusPanel(screen);
241
+ if (parsed.status !== "observed") {
242
+ return {
243
+ status: parsed.status,
244
+ screenFingerprint,
245
+ evidenceInventory: inventory.entries,
246
+ reason: parsed.reason
247
+ };
248
+ }
249
+ const evidenceFingerprint = fingerprintClaudeNativeInspection(parsed.region);
250
+ if (request.preEnterEvidenceInventory !== undefined) {
251
+ const priorOccurrences = request.preEnterEvidenceInventory.find((entry) => entry.evidenceFingerprint === evidenceFingerprint)?.occurrenceCount ?? 0;
252
+ const currentOccurrences = inventory.entries.find((entry) => entry.evidenceFingerprint === evidenceFingerprint)?.occurrenceCount ?? 0;
253
+ if (currentOccurrences <= priorOccurrences) {
254
+ return {
255
+ status: "stale",
256
+ nativeThreadId: parsed.nativeThreadId,
257
+ observedAgentVersion: parsed.agentVersion,
258
+ evidence: "claude_status_panel",
259
+ evidenceFingerprint,
260
+ screenFingerprint,
261
+ evidenceInventory: inventory.entries,
262
+ reason: "the Claude Status panel did not add fresh exact evidence after Enter"
263
+ };
264
+ }
265
+ }
266
+ if (parsed.agentVersion !== CLAUDE_NATIVE_INSPECTION_VERSION) {
267
+ return {
268
+ status: "mismatch",
269
+ nativeThreadId: parsed.nativeThreadId,
270
+ observedAgentVersion: parsed.agentVersion,
271
+ evidenceFingerprint,
272
+ screenFingerprint,
273
+ evidenceInventory: inventory.entries,
274
+ reason: `Claude /status reported unsupported version ${parsed.agentVersion}`
275
+ };
276
+ }
277
+ if (request.expectedAgentVersion !== undefined &&
278
+ parsed.agentVersion !== request.expectedAgentVersion) {
279
+ return {
280
+ status: "mismatch",
281
+ nativeThreadId: parsed.nativeThreadId,
282
+ observedAgentVersion: parsed.agentVersion,
283
+ evidenceFingerprint,
284
+ screenFingerprint,
285
+ evidenceInventory: inventory.entries,
286
+ reason: `Claude /status reported version ${parsed.agentVersion}, not the ` +
287
+ `verified running version ${request.expectedAgentVersion}`
288
+ };
289
+ }
290
+ if (!isExactNativeThreadId(request.expectedNativeThreadId)) {
291
+ return {
292
+ status: "mismatch",
293
+ nativeThreadId: parsed.nativeThreadId,
294
+ observedAgentVersion: parsed.agentVersion,
295
+ evidenceFingerprint,
296
+ screenFingerprint,
297
+ evidenceInventory: inventory.entries,
298
+ reason: "Claude native status inspection requires an exact claude agents Session identity"
299
+ };
300
+ }
301
+ if (parsed.nativeThreadId !== request.expectedNativeThreadId.toLowerCase()) {
302
+ return {
303
+ status: "mismatch",
304
+ nativeThreadId: parsed.nativeThreadId,
305
+ observedAgentVersion: parsed.agentVersion,
306
+ evidenceFingerprint,
307
+ screenFingerprint,
308
+ evidenceInventory: inventory.entries,
309
+ reason: `Claude /status reported ${parsed.nativeThreadId}, not the exact ` +
310
+ `claude agents Session ${request.expectedNativeThreadId.toLowerCase()}`
311
+ };
312
+ }
313
+ if (request.expectedCwd !== undefined &&
314
+ normalizeLifecyclePath(parsed.cwd) !==
315
+ normalizeLifecyclePath(request.expectedCwd)) {
316
+ return {
317
+ status: "mismatch",
318
+ nativeThreadId: parsed.nativeThreadId,
319
+ observedAgentVersion: parsed.agentVersion,
320
+ evidenceFingerprint,
321
+ screenFingerprint,
322
+ evidenceInventory: inventory.entries,
323
+ reason: `Claude /status cwd ${parsed.cwd} does not match the verified pane cwd`
324
+ };
325
+ }
326
+ return {
327
+ status: "observed",
328
+ nativeThreadId: parsed.nativeThreadId,
329
+ observedAgentVersion: parsed.agentVersion,
330
+ evidence: "claude_status_panel",
331
+ evidenceFingerprint,
332
+ screenFingerprint,
333
+ evidenceInventory: inventory.entries,
334
+ result: {
335
+ kind: "native_status",
336
+ nativeThreadId: parsed.nativeThreadId,
337
+ agentVersion: parsed.agentVersion,
338
+ fields: parsed.fields,
339
+ excerpt: claudeNativeStatusExcerpt(parsed.fields)
340
+ }
341
+ };
342
+ }
118
343
  export function probeClaudeThreadLifecycle(agentVersion) {
119
344
  if (!agentVersion) {
120
345
  return {
@@ -280,6 +505,205 @@ export function observeClaudeThreadLifecycle(requestOrScreen, legacyOperation, f
280
505
  reason: `Claude resumed ${nativeThreadId}, not the requested native thread ${expected}`
281
506
  };
282
507
  }
508
+ function parseCurrentClaudeStatusPanel(screen) {
509
+ const lines = screen.split("\n").slice(-CLAUDE_STATUS_PANEL_MAX_SCAN_LINES);
510
+ const headers = lines
511
+ .map((line, index) => CLAUDE_STATUS_PANEL_HEADER.test(line) ? index : -1)
512
+ .filter((index) => index >= 0);
513
+ if (headers.length === 0) {
514
+ return {
515
+ status: "missing",
516
+ reason: "no current Claude Settings Status panel is visible"
517
+ };
518
+ }
519
+ const headerIndex = headers.at(-1);
520
+ if (headerIndex === 0 ||
521
+ !CLAUDE_STATUS_PANEL_DIVIDER.test(lines[headerIndex - 1])) {
522
+ return {
523
+ status: "ambiguous",
524
+ reason: "the Claude Status header is not anchored to its exact panel divider"
525
+ };
526
+ }
527
+ const footerIndexes = lines
528
+ .map((line, index) => index > headerIndex &&
529
+ index - headerIndex <= CLAUDE_STATUS_PANEL_MAX_LINES &&
530
+ CLAUDE_STATUS_PANEL_FOOTER.test(line)
531
+ ? index
532
+ : -1)
533
+ .filter((index) => index >= 0);
534
+ if (footerIndexes.length !== 1) {
535
+ return {
536
+ status: "ambiguous",
537
+ reason: "the Claude Status panel is incomplete or has ambiguous dismissal markers"
538
+ };
539
+ }
540
+ const footerIndex = footerIndexes[0];
541
+ if (lines.slice(footerIndex + 1).some((line) => line.trim().length > 0)) {
542
+ return {
543
+ status: "missing",
544
+ reason: "the newest Claude Status panel is historical rather than current"
545
+ };
546
+ }
547
+ const regionLines = lines.slice(headerIndex, footerIndex + 1);
548
+ const region = regionLines.join("\n");
549
+ if (regionLines.length > CLAUDE_STATUS_PANEL_MAX_LINES ||
550
+ region.length > CLAUDE_STATUS_PANEL_MAX_REGION_LENGTH) {
551
+ return {
552
+ status: "ambiguous",
553
+ reason: "the Claude Status panel exceeds the bounded inspection region"
554
+ };
555
+ }
556
+ const rawFields = new Map();
557
+ for (const line of regionLines.slice(1, -1)) {
558
+ if (line.trim().length === 0) {
559
+ continue;
560
+ }
561
+ const match = CLAUDE_STATUS_PANEL_FIELD.exec(line);
562
+ if (!match) {
563
+ return {
564
+ status: "ambiguous",
565
+ reason: "the Claude Status panel contains an unprofiled non-field row"
566
+ };
567
+ }
568
+ const name = match[1].trim();
569
+ const value = match[2].trim();
570
+ if (!CLAUDE_STATUS_PANEL_FIELDS.has(name)) {
571
+ return {
572
+ status: "ambiguous",
573
+ reason: "the Claude Status panel contains an unprofiled field"
574
+ };
575
+ }
576
+ if (rawFields.has(name)) {
577
+ return {
578
+ status: "ambiguous",
579
+ reason: `the Claude Status panel repeats field ${name}`
580
+ };
581
+ }
582
+ rawFields.set(name, value);
583
+ }
584
+ if (rawFields.size === 0 || rawFields.size > CLAUDE_STATUS_PANEL_MAX_FIELDS) {
585
+ return {
586
+ status: "ambiguous",
587
+ reason: "the Claude Status panel has an invalid number of fields"
588
+ };
589
+ }
590
+ const agentVersion = rawFields.get("Version");
591
+ const nativeThreadId = rawFields.get("Session ID")?.toLowerCase();
592
+ const cwd = rawFields.get("cwd");
593
+ const model = rawFields.get("Model");
594
+ if (!agentVersion ||
595
+ !isExactNativeThreadId(nativeThreadId) ||
596
+ !cwd ||
597
+ !path.isAbsolute(cwd) ||
598
+ !model) {
599
+ return {
600
+ status: "ambiguous",
601
+ reason: "the Claude Status panel lacks exact Version, Session ID, cwd, or Model fields"
602
+ };
603
+ }
604
+ const fields = [...rawFields.entries()].map(([name, rawValue]) => ({
605
+ name,
606
+ value: redactClaudeNativeStatusField(name, rawValue)
607
+ .slice(0, CLAUDE_STATUS_PANEL_MAX_FIELD_VALUE_LENGTH)
608
+ }));
609
+ return {
610
+ status: "observed",
611
+ region,
612
+ agentVersion,
613
+ nativeThreadId,
614
+ cwd,
615
+ fields
616
+ };
617
+ }
618
+ function claudeStatusPanelEvidenceInventory(screen) {
619
+ const lines = screen.split("\n").slice(-CLAUDE_STATUS_PANEL_MAX_SCAN_LINES);
620
+ const counts = new Map();
621
+ for (let index = 0; index < lines.length; index += 1) {
622
+ if (!CLAUDE_STATUS_PANEL_HEADER.test(lines[index])) {
623
+ continue;
624
+ }
625
+ if (index === 0 ||
626
+ !CLAUDE_STATUS_PANEL_DIVIDER.test(lines[index - 1])) {
627
+ return {
628
+ status: "ambiguous",
629
+ entries: [],
630
+ reason: "a Claude Status marker is not anchored to its exact panel divider"
631
+ };
632
+ }
633
+ const footerIndex = lines.findIndex((line, candidateIndex) => candidateIndex > index &&
634
+ candidateIndex - index <= CLAUDE_STATUS_PANEL_MAX_LINES &&
635
+ CLAUDE_STATUS_PANEL_FOOTER.test(line));
636
+ if (footerIndex < 0) {
637
+ return {
638
+ status: "ambiguous",
639
+ entries: [],
640
+ reason: "a Claude Status panel marker has no bounded exact footer"
641
+ };
642
+ }
643
+ const region = lines.slice(index, footerIndex + 1).join("\n");
644
+ if (region.length > CLAUDE_STATUS_PANEL_MAX_REGION_LENGTH) {
645
+ return {
646
+ status: "ambiguous",
647
+ entries: [],
648
+ reason: "a Claude Status panel exceeds the bounded evidence region"
649
+ };
650
+ }
651
+ const fingerprint = fingerprintClaudeNativeInspection(region);
652
+ counts.set(fingerprint, (counts.get(fingerprint) ?? 0) + 1);
653
+ index = footerIndex;
654
+ }
655
+ const entries = [...counts.entries()].map(([evidenceFingerprint, occurrenceCount]) => ({ evidenceFingerprint, occurrenceCount }));
656
+ if (entries.length > CLAUDE_STATUS_PANEL_MAX_EVIDENCE_ENTRIES) {
657
+ return {
658
+ status: "ambiguous",
659
+ entries,
660
+ reason: "the Claude Status evidence inventory exceeds its bounded entry count"
661
+ };
662
+ }
663
+ return { status: "observed", entries };
664
+ }
665
+ function validateClaudeStatusEvidenceInventory(inventory) {
666
+ if (inventory === undefined) {
667
+ return undefined;
668
+ }
669
+ if (inventory.length > CLAUDE_STATUS_PANEL_MAX_EVIDENCE_ENTRIES) {
670
+ return "the pre-Enter Claude Status evidence inventory is over-bounded";
671
+ }
672
+ const seen = new Set();
673
+ for (const entry of inventory) {
674
+ if (!NATIVE_INSPECTION_FINGERPRINT.test(entry.evidenceFingerprint) ||
675
+ !Number.isSafeInteger(entry.occurrenceCount) ||
676
+ entry.occurrenceCount < 1 ||
677
+ seen.has(entry.evidenceFingerprint)) {
678
+ return "the pre-Enter Claude Status evidence inventory is malformed";
679
+ }
680
+ seen.add(entry.evidenceFingerprint);
681
+ }
682
+ return undefined;
683
+ }
684
+ function redactClaudeNativeStatusField(name, value) {
685
+ if (CLAUDE_STATUS_PANEL_SENSITIVE_FIELDS.has(name) ||
686
+ /(?:auth|account|email|api\s*key|token|credential|login|organization|user)/iu
687
+ .test(name)) {
688
+ return "[REDACTED]";
689
+ }
690
+ return redactString(value).replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/giu, "[REDACTED EMAIL]");
691
+ }
692
+ function claudeNativeStatusExcerpt(fields) {
693
+ return [
694
+ "Claude Code native status",
695
+ ...fields.map((field) => `${field.name}: ${field.value}`)
696
+ ].join("\n").slice(0, CLAUDE_STATUS_PANEL_MAX_EXCERPT_LENGTH);
697
+ }
698
+ function normalizeClaudeNativeInspectionScreen(screen) {
699
+ return screen
700
+ .replace(ANSI_ESCAPE_PATTERN, "")
701
+ .replace(/\r\n?/gu, "\n")
702
+ .replace(/\u00a0/gu, " ");
703
+ }
704
+ function fingerprintClaudeNativeInspection(value) {
705
+ return `sha256:${createHash("sha256").update(value).digest("hex")}`;
706
+ }
283
707
  function normalizeLifecyclePath(value) {
284
708
  const candidate = nonEmptyString(value);
285
709
  return candidate === undefined ? undefined : path.resolve(candidate);