@scotthuang/agent-knock-knock 0.4.0 → 0.5.0

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/dist/src/cli.js CHANGED
@@ -11,7 +11,7 @@ import { captureClaudeTranscriptAnchor, defaultClaudeHome, detectClaudeTranscrip
11
11
  import { CodexLocalSessionProvider } from "./codex-local-session-provider.js";
12
12
  import { CodexStoreAdapter } from "./codex-store-adapter.js";
13
13
  import { applyMessageToConversation, budgetAction, createConversation, createMessage, executorForConversation, extractStructuredMessage, parseMessageJson, resolveExecutor } from "./protocol.js";
14
- import { EXECUTOR_KINDS, executorDefinitionForKind, isExecutorKind } from "./executors.js";
14
+ import { EXECUTOR_KINDS, executorDefinitionForKind } from "./executors.js";
15
15
  import { redactString, writeRuntimeLog } from "./runtime-log.js";
16
16
  import { formatTranscript, readNdjsonLog } from "./transcript.js";
17
17
  import { appendEvent, defaultStoreDir, ensureDir, listConversations, logPathForStatePath, loadConversationById, loadState, messageEvent, pathsForConversation, pathsForConversationDir, saveState, statePathForConversationId } from "./store.js";
@@ -70,8 +70,6 @@ const CONVERSATION_STATUSES = new Set([
70
70
  ]);
71
71
  const SESSION_SELECTOR_COMMANDS = new Set([
72
72
  "status",
73
- "describe",
74
- "summary",
75
73
  "send",
76
74
  "approve",
77
75
  "cancel",
@@ -110,9 +108,7 @@ class InlineCodexSessionAdapter {
110
108
  }
111
109
  const command = process.argv[2];
112
110
  const rawArgs = process.argv.slice(3);
113
- const args = command === "agent"
114
- ? { agentCommand: rawArgs[0], ...parseArgs(rawArgs.slice(1)) }
115
- : parseArgs(rawArgs);
111
+ const args = parseArgs(rawArgs);
116
112
  runtimeLog("info", "cli_start", {
117
113
  command: command ?? "help",
118
114
  cwd: process.cwd(),
@@ -151,9 +147,6 @@ async function runCommand(commandName, options) {
151
147
  else if (commandName === "status") {
152
148
  await runStatus(options);
153
149
  }
154
- else if (commandName === "describe" || commandName === "summary") {
155
- await runDescribe(options);
156
- }
157
150
  else if (commandName === "send") {
158
151
  await runSend(options);
159
152
  }
@@ -190,9 +183,6 @@ async function runCommand(commandName, options) {
190
183
  else if (commandName === "monitor") {
191
184
  await runMonitor(options);
192
185
  }
193
- else if (commandName === "agent") {
194
- await runAgent(options);
195
- }
196
186
  else {
197
187
  usage();
198
188
  process.exitCode = commandName ? 1 : 0;
@@ -204,15 +194,16 @@ function runInstallOpenClaw(options) {
204
194
  const workspace = options.workspace === undefined
205
195
  ? undefined
206
196
  : canonicalWorkspace(options.workspace);
207
- const defaultAgent = optionalExecutorKind(options.defaultAgent);
197
+ if (options.defaultAgent !== undefined) {
198
+ throw new Error("--default-agent was removed; AKK now selects the only eligible idle tmux pane");
199
+ }
208
200
  if (options.mode !== undefined) {
209
201
  throw new Error("--mode was removed; Agent Knock Knock now uses tmux only");
210
202
  }
211
203
  if (skillOnly &&
212
204
  (workspace !== undefined ||
213
- defaultAgent !== undefined ||
214
205
  options.verify === true)) {
215
- throw new Error("--skill-only cannot be combined with --workspace, --default-agent, --mode, or --verify");
206
+ throw new Error("--skill-only cannot be combined with --workspace, --mode, or --verify");
216
207
  }
217
208
  const needsOpenClaw = !skillOnly || options.noRestart !== true || options.verify === true;
218
209
  const openclawBin = needsOpenClaw
@@ -238,13 +229,7 @@ function runInstallOpenClaw(options) {
238
229
  : [{
239
230
  path: "plugins.entries.agent-knock-knock.config.workspace",
240
231
  value: workspace
241
- }]),
242
- ...(defaultAgent === undefined
243
- ? []
244
- : [{
245
- path: "plugins.entries.agent-knock-knock.config.defaultAgent",
246
- value: defaultAgent
247
- }]),
232
+ }])
248
233
  ];
249
234
  runCheckedCommand(openclawBin, ["config", "set", "--batch-json", JSON.stringify(configOperations)], { label: "openclaw config set" });
250
235
  steps.push({
@@ -280,8 +265,7 @@ function runInstallOpenClaw(options) {
280
265
  : false;
281
266
  const nextActions = installNextActions({
282
267
  pendingRestart,
283
- verification,
284
- agent: defaultAgent ?? "codex"
268
+ verification
285
269
  });
286
270
  printJson({
287
271
  installed: true,
@@ -289,7 +273,6 @@ function runInstallOpenClaw(options) {
289
273
  pending_restart: pendingRestart,
290
274
  mode: skillOnly ? "skill_only" : "full",
291
275
  execution_mode: "tmux",
292
- default_agent: defaultAgent ?? null,
293
276
  workspace: workspace ?? null,
294
277
  package_root: root,
295
278
  openclaw_bin: openclawBin ?? null,
@@ -314,17 +297,38 @@ function canonicalWorkspace(value) {
314
297
  }
315
298
  return canonical;
316
299
  }
317
- function optionalExecutorKind(value) {
318
- if (value === undefined) {
319
- return undefined;
300
+ function matchesConfiguredWorkspace(configuredWorkspace, candidateWorkspace) {
301
+ if (configuredWorkspace === undefined) {
302
+ return true;
320
303
  }
321
- const normalized = String(value).trim().toLowerCase();
322
- if (!isExecutorKind(normalized)) {
323
- throw new Error(`--default-agent must be one of: ${EXECUTOR_KINDS.join(", ")}`);
304
+ if (candidateWorkspace === undefined || candidateWorkspace === null) {
305
+ return false;
306
+ }
307
+ try {
308
+ return canonicalWorkspace(configuredWorkspace) ===
309
+ canonicalWorkspace(candidateWorkspace);
310
+ }
311
+ catch {
312
+ return false;
324
313
  }
325
- return normalized;
326
314
  }
327
- function installNextActions({ pendingRestart, verification, agent }) {
315
+ function assertConfiguredWorkspace(configuredWorkspace, candidateWorkspace, subject) {
316
+ if (configuredWorkspace === undefined) {
317
+ return;
318
+ }
319
+ const configured = canonicalWorkspace(configuredWorkspace);
320
+ let candidate;
321
+ try {
322
+ candidate = canonicalWorkspace(candidateWorkspace);
323
+ }
324
+ catch {
325
+ throw new Error(`refusing ${subject}; its workspace cannot be verified against configured workspace ${configured}`);
326
+ }
327
+ if (candidate !== configured) {
328
+ throw new Error(`refusing ${subject}; workspace ${candidate} does not match configured workspace ${configured}`);
329
+ }
330
+ }
331
+ function installNextActions({ pendingRestart, verification }) {
328
332
  if (pendingRestart) {
329
333
  return [
330
334
  {
@@ -355,8 +359,8 @@ function installNextActions({ pendingRestart, verification, agent }) {
355
359
  }];
356
360
  }
357
361
  return [{
358
- action: "start_agent",
359
- command: `tmux new -s akk-${agent} ${agent}`
362
+ action: "start_shared_terminal",
363
+ command: "tmux new -s akk-work -c \"$PWD\" codex # use claude instead when preferred"
360
364
  }];
361
365
  }
362
366
  function installOpenClawPlugin(openclawBin, root) {
@@ -438,13 +442,7 @@ function buildDoctorReport(options) {
438
442
  ...(options.workspace ? { workspace: String(options.workspace) } : {}),
439
443
  ...(timeoutMs === undefined ? {} : { timeoutMs })
440
444
  });
441
- const selectedAgent = optionalExecutorKind(options.defaultAgent) ??
442
- openclaw.default_agent ??
443
- "codex";
444
- const selectedAgentReady = capabilities.tmux.status === "ready" &&
445
- capabilities.tmux.agents.includes(selectedAgent);
446
445
  const ok = capabilities.readiness === "ready" &&
447
- selectedAgentReady &&
448
446
  filesOk &&
449
447
  openclaw.ready;
450
448
  return {
@@ -455,12 +453,12 @@ function buildDoctorReport(options) {
455
453
  ? "not_ready"
456
454
  : "partially_ready",
457
455
  selected_mode: "tmux",
458
- selected_agent: selectedAgent
459
- ? {
460
- agent: selectedAgent,
461
- ready: selectedAgentReady
462
- }
463
- : null,
456
+ live_terminal: {
457
+ checked: false,
458
+ required_for_install_readiness: false,
459
+ detail: "Installation readiness checks tmux and at least one supported CLI. " +
460
+ "AKK verifies a live eligible pane when delegation begins."
461
+ },
464
462
  package_root: root,
465
463
  checks,
466
464
  package_files: packageFiles,
@@ -471,6 +469,7 @@ function buildDoctorReport(options) {
471
469
  notes: [
472
470
  `Node.js ${MINIMUM_NODE_VERSION}+ and OpenClaw are required.`,
473
471
  "AKK supports Codex and Claude Code through shared tmux terminals.",
472
+ "Doctor does not require a live coding-agent pane; delegation discovers and verifies one at send time.",
474
473
  "Claude tmux completion is hook-free and fails closed unless the local transcript schema is verified."
475
474
  ]
476
475
  };
@@ -497,88 +496,6 @@ function versionAtLeast(version, minimum) {
497
496
  }
498
497
  return true;
499
498
  }
500
- async function runAgent(options) {
501
- const agentCommand = required(options.agentCommand, "agent subcommand is required: takeover");
502
- if (agentCommand === "takeover") {
503
- printJson(await runAgentTakeover(options));
504
- return;
505
- }
506
- throw new Error(`unsupported agent subcommand: ${agentCommand}`);
507
- }
508
- async function runAgentTakeover(options) {
509
- const agent = required(options.agent, "--agent is required");
510
- const sessionId = required(options.sessionId, "--session-id is required");
511
- const strategy = options.strategy ?? "terminal_control";
512
- if (strategy !== "terminal_control") {
513
- throw new Error("Agent Knock Knock only supports terminal_control takeover through tmux");
514
- }
515
- const provider = createAgentSessionProvider(agent, options);
516
- const session = await provider.getSession(sessionId);
517
- if (!session) {
518
- return {
519
- agent,
520
- sessionId,
521
- strategy,
522
- status: "blocked",
523
- sideEffectsExecuted: false,
524
- error: {
525
- code: "session_not_found",
526
- message: `No ${agent} session found for ${sessionId}`
527
- }
528
- };
529
- }
530
- const activeSessions = await listActiveSessionsWithTerminalControl(provider, options);
531
- const plan = planTerminalControlTakeover(session, activeSessions);
532
- if (options.confirmTerminal === true) {
533
- const terminalTarget = String(required(options.terminalTarget, "--terminal-target is required with --confirm-terminal"));
534
- if (!options.createConversation) {
535
- throw new Error("--create-conversation is required with --confirm-terminal");
536
- }
537
- const target = plan.targets.find((candidate) => candidate.terminalControl?.target === terminalTarget);
538
- if (!plan.allowed || !target?.terminalControl) {
539
- return {
540
- agent,
541
- sessionId,
542
- strategy,
543
- status: "blocked",
544
- sideEffectsExecuted: false,
545
- plan,
546
- error: {
547
- code: "terminal_target_unavailable",
548
- message: `No matching terminal-controlled ${agent} process was found for ${terminalTarget}`
549
- }
550
- };
551
- }
552
- const attached = createNativeSessionConversation({
553
- agent,
554
- strategy,
555
- session,
556
- options,
557
- takeoverMatchKind: "terminal_control",
558
- terminalControl: target.terminalControl,
559
- terminalAgentPid: target.pid,
560
- needsBootstrap: false
561
- });
562
- return {
563
- agent,
564
- sessionId,
565
- strategy,
566
- status: "attached",
567
- sideEffectsExecuted: true,
568
- plan,
569
- terminalControl: target.terminalControl,
570
- ...attached
571
- };
572
- }
573
- return {
574
- agent,
575
- sessionId,
576
- strategy,
577
- status: plan.allowed ? "requires_confirmation" : "blocked",
578
- sideEffectsExecuted: false,
579
- plan
580
- };
581
- }
582
499
  async function listActiveSessionsWithTerminalControl(provider, options, terminalProvider = createTerminalControlProvider(options)) {
583
500
  const activeSessions = await provider.listActiveSessions();
584
501
  const activePids = new Set(activeSessions.map((session) => session.pid));
@@ -736,7 +653,9 @@ function createTerminalAgentBridge(options, terminalProvider = createTerminalCon
736
653
  terminalProvider,
737
654
  async verifyIdentity({ agent, pid, terminalControl }) {
738
655
  const adapter = registry.require(agent);
739
- const snapshots = await processSource.listProcessSnapshots(undefined, { includeCwd: false });
656
+ const snapshots = await processSource.listProcessSnapshots(undefined, {
657
+ includeCwd: options.workspace !== undefined
658
+ });
740
659
  const snapshot = snapshots.find((candidate) => candidate.pid === pid);
741
660
  if (!snapshot || !adapter.classifyProcess(snapshot)) {
742
661
  throw new Error(`terminal conversation agent ${agent} with pid ${pid} is no longer active`);
@@ -748,6 +667,8 @@ function createTerminalAgentBridge(options, terminalProvider = createTerminalCon
748
667
  if (!pane || !terminalPaneContainsProcess(snapshot, pane, snapshots)) {
749
668
  throw new Error(`terminal conversation agent ${agent} with pid ${pid} no longer belongs to pane ${terminalControl.target}`);
750
669
  }
670
+ assertConfiguredWorkspace(options.workspace, snapshot.cwd, `terminal access to ${terminalControl.target} by agent process ${pid}`);
671
+ assertConfiguredWorkspace(options.workspace, pane.currentPath, `terminal access to ${terminalControl.target} by tmux pane`);
751
672
  return {
752
673
  terminalControl: {
753
674
  ...terminalControl,
@@ -760,39 +681,6 @@ function createTerminalAgentBridge(options, terminalProvider = createTerminalCon
760
681
  }
761
682
  });
762
683
  }
763
- function planTerminalControlTakeover(session, activeSessions) {
764
- const matched = activeSessions
765
- .filter((process) => process.kind === "codex_cli" &&
766
- process.terminalControl &&
767
- (process.sessionId === session.id ||
768
- (!process.sessionId && process.cwd === session.cwd)));
769
- const matchedPidSet = new Set(matched.map((process) => process.pid));
770
- const targets = matched
771
- .filter((process) => !process.ppid || !matchedPidSet.has(process.ppid))
772
- .map((process) => ({
773
- pid: process.pid,
774
- childPids: matched
775
- .filter((child) => child.ppid === process.pid)
776
- .map((child) => child.pid),
777
- cwd: process.cwd,
778
- command: process.command,
779
- sessionId: process.sessionId,
780
- terminalControl: process.terminalControl
781
- }));
782
- const exactTargets = targets.filter((target) => target.sessionId === session.id);
783
- const selectableTargets = exactTargets.length > 0 ? exactTargets : targets;
784
- return {
785
- mode: "terminal_control",
786
- allowed: selectableTargets.length === 1,
787
- requiresConfirmation: selectableTargets.length === 1,
788
- reason: selectableTargets.length === 0
789
- ? "no_terminal_control_target"
790
- : selectableTargets.length === 1
791
- ? "terminal_control_available"
792
- : "ambiguous_terminal_control_target",
793
- targets: selectableTargets
794
- };
795
- }
796
684
  function terminalControlFromTakeover(nativeTakeover) {
797
685
  if (!isRecord(nativeTakeover)) {
798
686
  return undefined;
@@ -992,88 +880,19 @@ function isTerminalControlCapability(value) {
992
880
  "terminal_cancel"
993
881
  ].includes(value);
994
882
  }
995
- function createNativeSessionConversation({ agent, strategy, session, options, takeoverMatchKind = strategy, terminalControl = undefined, terminalAgentPid = undefined, needsBootstrap = false }) {
996
- const workspace = session.cwd;
997
- const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(workspace));
998
- cleanupIdleConversations(storeDir, options);
999
- const executor = resolveExecutor({
1000
- kind: agent,
1001
- session: session.id
1002
- });
1003
- const now = new Date();
1004
- const conversation = createConversation({
1005
- userRequest: options.request ?? `Attach native ${agent} session ${session.id}`,
1006
- workspace,
1007
- openclawSession: options.openclawSession ?? "agent:main:main",
1008
- executorKind: executor.kind,
1009
- executorSession: executor.session,
1010
- softLimit: Number(options.softLimit ?? 50),
1011
- hardLimit: Number(options.hardLimit ?? 100),
1012
- now
1013
- });
1014
- const paths = pathsForConversation(conversation.conversation_id, storeDir);
1015
- const attachedConversation = withStoragePaths({
1016
- ...conversation,
1017
- executor,
1018
- status: "idle",
1019
- idle_since: now.toISOString(),
1020
- updated_at: now.toISOString(),
1021
- gateway_url: options.gatewayUrl ?? "ws://127.0.0.1:18789",
1022
- gateway_method: options.gatewayMethod,
1023
- gateway_session: options.gatewaySession ?? options.openclawSession ?? "agent:main:main",
1024
- openclaw_bin: options.openclawBin ?? resolveOptionalExecutable("openclaw"),
1025
- native_session_takeover: {
1026
- agent,
1027
- native_session_id: session.id,
1028
- terminal_agent_pid: terminalAgentPid,
1029
- source_cwd: session.cwd,
1030
- source_title: session.title,
1031
- strategy,
1032
- attached_at: now.toISOString(),
1033
- takeover_match_kind: takeoverMatchKind,
1034
- terminal_control: terminalControl,
1035
- needs_bootstrap: needsBootstrap,
1036
- terminal_bridge: true
1037
- }
1038
- }, paths);
1039
- saveState(paths.statePath, attachedConversation);
1040
- appendEvent(paths.logPath, {
1041
- ts: now.toISOString(),
1042
- conversation_id: attachedConversation.conversation_id,
1043
- event: "native_session_attached",
1044
- agent,
1045
- strategy,
1046
- native_session_id: session.id,
1047
- source_cwd: session.cwd,
1048
- executor
1049
- });
1050
- runtimeLog("info", "native_session_attached", {
1051
- conversation_id: attachedConversation.conversation_id,
1052
- agent,
1053
- strategy,
1054
- native_session_id: session.id,
1055
- state_path: paths.statePath,
1056
- event_log_path: paths.logPath
1057
- });
1058
- return {
1059
- conversation: attachedConversation,
1060
- paths,
1061
- next: `Use AKK send ${attachedConversation.conversation_id}: <message> to continue this native ${agent} session through AKK.`
1062
- };
1063
- }
1064
883
  async function runDelegate(options) {
1065
884
  const request = required(options.request, "--request is required");
1066
885
  const workspace = canonicalWorkspace(options.workspace ?? process.cwd());
1067
- const executor = resolveExecutor({
1068
- kind: options.agent ?? "codex"
1069
- });
1070
- const scan = await buildNativeListGroups({
886
+ const requestedAgent = options.agent === undefined
887
+ ? undefined
888
+ : resolveExecutor({ kind: options.agent }).kind;
889
+ const scan = await buildTerminalListGroup({
1071
890
  options: {
1072
891
  ...options,
1073
892
  workspace,
1074
893
  noApprovalScan: false
1075
894
  },
1076
- agentFilter: executor.kind,
895
+ agentFilter: requestedAgent,
1077
896
  statusFilter: undefined
1078
897
  });
1079
898
  if (scan.summary.error) {
@@ -1092,15 +911,21 @@ async function runDelegate(options) {
1092
911
  const observed = sameWorkspace.length > 0
1093
912
  ? ` Found ${sameWorkspace.length} matching pane(s), but none is idle.`
1094
913
  : "";
1095
- throw new Error(`No idle ${executor.display_name} pane is available in ${workspace}.${observed} ` +
1096
- `Start ${executor.kind} inside tmux in that workspace, wait until it is idle, then retry.`);
914
+ const requestedExecutor = requestedAgent
915
+ ? executorDefinitionForKind(requestedAgent)
916
+ : undefined;
917
+ throw new Error(`No idle ${requestedExecutor?.displayName ?? "Codex or Claude Code"} pane is available in ${workspace}.${observed} ` +
918
+ `Start ${requestedAgent ?? "codex or claude"} inside tmux in that workspace, wait until it is idle, then retry.`);
1097
919
  }
1098
920
  if (eligible.length > 1) {
1099
921
  const candidates = eligible
1100
- .map((candidate) => `${candidate.short_ref} (${candidate.terminal_control?.target ?? candidate.id})`)
922
+ .map((candidate) => `${candidate.short_ref} (${candidate.agent}, ${candidate.terminal_control?.target ?? candidate.id})`)
1101
923
  .join(", ");
1102
- throw new Error(`Multiple idle ${executor.display_name} panes match ${workspace}: ${candidates}. ` +
1103
- "Use /akk list and /akk send <session>: <message> to choose one explicitly.");
924
+ const scope = requestedAgent
925
+ ? executorDefinitionForKind(requestedAgent).displayName
926
+ : "coding-agent";
927
+ throw new Error(`Multiple idle ${scope} panes match ${workspace}: ${candidates}. ` +
928
+ "Use /akk codex: <task>, /akk claude: <task>, or /akk @short-ref: <message> to choose one explicitly.");
1104
929
  }
1105
930
  await runSend({
1106
931
  ...options,
@@ -1495,35 +1320,49 @@ async function runList(options) {
1495
1320
  const statusFilter = options.status;
1496
1321
  const storedConversations = listConversations(storeDir)
1497
1322
  .filter((conversation) => includeAll || isActiveStatus(conversation.status))
1323
+ .filter((conversation) => matchesConfiguredWorkspace(options.workspace, conversation.workspace))
1498
1324
  .filter((conversation) => !agentFilter || executorForConversation(conversation).kind === agentFilter)
1499
1325
  .filter((conversation) => !statusFilter || conversation.status === statusFilter);
1500
1326
  const conversations = storedConversations.map((conversation) => summarizeConversation(conversation));
1501
1327
  const delegated = storedConversations.map((conversation) => delegatedListEntry(summarizeConversation(conversation), { terminalBridge: terminalBridgeEnabled(conversation) }));
1502
- const nativeScan = await buildNativeListGroups({ options, agentFilter, statusFilter });
1328
+ const terminalScan = await buildTerminalListGroup({ options, agentFilter, statusFilter });
1329
+ const managedTerminalKeys = new Set(storedConversations
1330
+ .filter((conversation) => isActiveStatus(conversation.status))
1331
+ .map((conversation) => terminalControlSelectorKey(terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
1332
+ ? conversation.native_session_takeover
1333
+ : undefined)))
1334
+ .filter((key) => key !== undefined));
1335
+ const terminalControlled = terminalScan.terminalControlled.filter((entry) => {
1336
+ if (!matchesConfiguredWorkspace(options.workspace, entry.workspace ?? entry.cwd)) {
1337
+ return false;
1338
+ }
1339
+ const key = terminalControlSelectorKey(entry.terminal_control);
1340
+ return key === undefined || !managedTerminalKeys.has(key);
1341
+ });
1503
1342
  printJson({
1504
1343
  store_dir: storeDir,
1505
1344
  cleanup,
1506
1345
  delegated,
1507
- native: nativeScan.native,
1508
- terminal_controlled: nativeScan.terminalControlled,
1509
- native_scan: nativeScan.summary,
1346
+ terminal_controlled: terminalControlled,
1347
+ terminal_scan: {
1348
+ ...terminalScan.summary,
1349
+ terminal_controlled_count: terminalControlled.length
1350
+ },
1510
1351
  tasks: conversations
1511
1352
  });
1512
1353
  runtimeLog("info", "tasks_listed", {
1513
1354
  store_dir: storeDir,
1514
1355
  returned_count: conversations.length,
1515
- native_count: nativeScan.native.length,
1516
- terminal_controlled_count: nativeScan.terminalControlled.length,
1517
- native_scan_error: nativeScan.summary.error,
1356
+ terminal_controlled_count: terminalControlled.length,
1357
+ terminal_scan_error: terminalScan.summary.error,
1518
1358
  include_all: includeAll,
1519
1359
  agent_filter: agentFilter,
1520
1360
  status_filter: statusFilter,
1521
1361
  cleanup
1522
1362
  });
1523
1363
  }
1524
- async function buildNativeListGroups({ options, agentFilter, statusFilter }) {
1364
+ async function buildTerminalListGroup({ options, agentFilter, statusFilter }) {
1525
1365
  const empty = {
1526
- native: [],
1527
1366
  terminalControlled: [],
1528
1367
  summary: {
1529
1368
  enabled: false,
@@ -1540,7 +1379,7 @@ async function buildNativeListGroups({ options, agentFilter, statusFilter }) {
1540
1379
  summary: {
1541
1380
  enabled: false,
1542
1381
  agents: [],
1543
- skipped: `native active discovery skipped for status filter ${statusFilter}`
1382
+ skipped: `terminal discovery skipped for status filter ${statusFilter}`
1544
1383
  }
1545
1384
  };
1546
1385
  }
@@ -1560,40 +1399,35 @@ async function buildNativeListGroups({ options, agentFilter, statusFilter }) {
1560
1399
  }
1561
1400
  const terminalProvider = createTerminalControlProvider(options);
1562
1401
  const bridge = createTerminalAgentBridge(options, terminalProvider, registry);
1563
- const terminalScan = options.terminalDebug ? await terminalControlDiagnostics(terminalProvider) : undefined;
1402
+ const terminalDiagnostics = options.terminalDebug
1403
+ ? await terminalControlDiagnostics(terminalProvider)
1404
+ : undefined;
1564
1405
  const terminalControlled = [];
1565
- const native = [];
1566
1406
  let activeCount = 0;
1567
1407
  const errors = [];
1568
1408
  try {
1569
1409
  const processSource = createTerminalProcessSource(options);
1570
1410
  const snapshots = await processSource.listProcessSnapshots((snapshot) => adapters.some((adapter) => adapter.capabilities.processDiscovery && adapter.classifyProcess(snapshot) !== undefined), { includeAncestors: true });
1571
1411
  const activeSessions = await bridge.listProcesses(snapshots, adapters.map((adapter) => adapter.agent));
1572
- activeCount = activeSessions.length;
1573
1412
  const rootSessions = rootActiveProcesses(activeSessions);
1574
- for (const session of rootSessions) {
1575
- if (session.terminalControl) {
1576
- terminalControlled.push(await terminalControlledListEntry(session, activeSessions, options, bridge));
1577
- }
1578
- else {
1579
- native.push(nativeListEntry(session, activeSessions));
1580
- }
1413
+ const controlledSessions = rootSessions.filter((session) => session.terminalControl !== undefined);
1414
+ activeCount = controlledSessions.length;
1415
+ for (const session of controlledSessions) {
1416
+ terminalControlled.push(await terminalControlledListEntry(session, activeSessions, options, bridge));
1581
1417
  }
1582
1418
  }
1583
1419
  catch (error) {
1584
1420
  errors.push(error instanceof Error ? error.message : String(error));
1585
1421
  }
1586
1422
  return {
1587
- native,
1588
1423
  terminalControlled,
1589
1424
  summary: {
1590
1425
  enabled: true,
1591
1426
  agents: adapters.map((adapter) => adapter.agent),
1592
1427
  active_count: activeCount,
1593
- native_count: native.length,
1594
1428
  terminal_controlled_count: terminalControlled.length,
1595
1429
  approval_scan: options.noApprovalScan ? "disabled" : "enabled",
1596
- terminal_scan: terminalScan,
1430
+ diagnostics: terminalDiagnostics,
1597
1431
  error: errors.length > 0 ? errors.join("; ") : undefined
1598
1432
  }
1599
1433
  };
@@ -1622,33 +1456,6 @@ function delegatedListEntry(task, { terminalBridge = false } = {}) {
1622
1456
  }
1623
1457
  };
1624
1458
  }
1625
- function nativeListEntry(session, activeSessions) {
1626
- const id = `native:${session.agent}:${session.pid}`;
1627
- return {
1628
- id,
1629
- short_ref: sessionShortRef(id),
1630
- source: "native_active",
1631
- agent: session.agent,
1632
- status: "active",
1633
- pid: session.pid,
1634
- child_pids: childPidsForRoot(session, activeSessions),
1635
- command: session.command,
1636
- cwd: session.cwd,
1637
- workspace: session.cwd,
1638
- elapsed: session.elapsed,
1639
- session_id: session.sessionId,
1640
- confidence: session.confidence,
1641
- reason: session.reason,
1642
- commands: {
1643
- terminal_control_attach: false,
1644
- send: false,
1645
- cancel: false,
1646
- approve: false,
1647
- close: false,
1648
- status: false
1649
- }
1650
- };
1651
- }
1652
1459
  async function terminalControlledListEntry(session, activeSessions, options, bridge = createTerminalAgentBridge(options)) {
1653
1460
  const terminalControl = session.terminalControl;
1654
1461
  if (!terminalControl) {
@@ -1795,14 +1602,16 @@ async function sessionSelectorCandidates(commandName, options) {
1795
1602
  const storeDir = storeDirFromOptions(options);
1796
1603
  cleanupIdleConversations(storeDir, options);
1797
1604
  const storedConversations = listConversations(storeDir);
1798
- const managed = storedConversations.map((conversation) => delegatedListEntry(summarizeConversation(conversation), { terminalBridge: terminalBridgeEnabled(conversation) }));
1799
- const managedTerminalKeys = new Set(storedConversations
1605
+ const workspaceConversations = storedConversations
1606
+ .filter((conversation) => matchesConfiguredWorkspace(options.workspace, conversation.workspace));
1607
+ const managed = workspaceConversations.map((conversation) => delegatedListEntry(summarizeConversation(conversation), { terminalBridge: terminalBridgeEnabled(conversation) }));
1608
+ const managedTerminalKeys = new Set(workspaceConversations
1800
1609
  .filter((conversation) => isActiveStatus(conversation.status))
1801
1610
  .map((conversation) => terminalControlSelectorKey(terminalControlFromTakeover(isRecord(conversation.native_session_takeover)
1802
1611
  ? conversation.native_session_takeover
1803
1612
  : undefined)))
1804
1613
  .filter((key) => key !== undefined));
1805
- const nativeScan = await buildNativeListGroups({
1614
+ const terminalScan = await buildTerminalListGroup({
1806
1615
  options: {
1807
1616
  ...options,
1808
1617
  noApprovalScan: commandName === "approve"
@@ -1815,11 +1624,13 @@ async function sessionSelectorCandidates(commandName, options) {
1815
1624
  const observedAtMs = Date.now();
1816
1625
  return [
1817
1626
  ...managed,
1818
- ...nativeScan.terminalControlled.filter((entry) => {
1627
+ ...terminalScan.terminalControlled.filter((entry) => {
1628
+ if (!matchesConfiguredWorkspace(options.workspace, entry.workspace ?? entry.cwd)) {
1629
+ return false;
1630
+ }
1819
1631
  const key = terminalControlSelectorKey(entry.terminal_control);
1820
1632
  return key === undefined || !managedTerminalKeys.has(key);
1821
- }),
1822
- ...nativeScan.native
1633
+ })
1823
1634
  ].map((entry) => ({
1824
1635
  id: String(entry.id),
1825
1636
  agent: resolveExecutor({ kind: entry.agent }).kind,
@@ -1847,16 +1658,10 @@ function terminalControlSelectorKey(value) {
1847
1658
  });
1848
1659
  }
1849
1660
  function sessionEntrySupportsCommand(entry, commandName) {
1850
- if (commandName === "summary") {
1851
- commandName = "describe";
1852
- }
1853
1661
  const commands = isRecord(entry.commands) ? entry.commands : {};
1854
1662
  if (typeof commands[commandName] === "boolean") {
1855
1663
  return commands[commandName] === true;
1856
1664
  }
1857
- if (commandName === "describe") {
1858
- return commands.status === true || entry.source === "native_active";
1859
- }
1860
1665
  if (entry.source !== "akk_delegate") {
1861
1666
  return false;
1862
1667
  }
@@ -1882,21 +1687,6 @@ function sessionEntryRecency(entry, observedAtMs) {
1882
1687
  async function resolveTerminalConversationFromOptions(options) {
1883
1688
  return createTerminalAgentBridge(options).resolveConversationId(stringValue(options.conversation ?? options.conversationId));
1884
1689
  }
1885
- function parseNativeConversationId(conversationId) {
1886
- const prefix = "native:codex:";
1887
- if (!conversationId?.startsWith(prefix)) {
1888
- return undefined;
1889
- }
1890
- const pid = Number(conversationId.slice(prefix.length));
1891
- if (!Number.isInteger(pid)) {
1892
- throw new Error(`invalid native conversation id: ${conversationId}`);
1893
- }
1894
- return {
1895
- conversationId,
1896
- agent: "codex",
1897
- pid
1898
- };
1899
- }
1900
1690
  async function runStatus(options) {
1901
1691
  cleanupIdleConversations(storeDirFromOptions(options), options);
1902
1692
  const terminalConversation = await resolveTerminalConversationFromOptions(options);
@@ -1907,9 +1697,12 @@ async function runStatus(options) {
1907
1697
  conversationId: terminalConversation.conversationId,
1908
1698
  terminalTarget: terminalConversation.terminalControl.target
1909
1699
  });
1700
+ const context = await terminalStatusContext(terminalConversation, terminalStatus, options);
1910
1701
  printJson({
1911
1702
  conversation_id: terminalConversation.conversationId,
1912
1703
  source: "terminal_control",
1704
+ agent: terminalConversation.agent,
1705
+ ...context,
1913
1706
  terminal_control: terminalConversation.terminalControl,
1914
1707
  terminal_status: terminalStatus,
1915
1708
  terminal_screen: terminalStatus.screen
@@ -1931,6 +1724,9 @@ async function runStatus(options) {
1931
1724
  const result = {
1932
1725
  conversation,
1933
1726
  summary: summarizeConversation(conversation),
1727
+ confidence: "high",
1728
+ about: managedConversationAbout(conversation, events),
1729
+ limitations: [],
1934
1730
  state_path: statePath,
1935
1731
  event_log_path: logPath,
1936
1732
  budget: budgetAction(conversation),
@@ -1945,6 +1741,13 @@ async function runStatus(options) {
1945
1741
  result.terminal_control = terminalControl;
1946
1742
  result.terminal_status = await terminalStatusForControl(executor.kind, terminalControl, options, terminalRuntimeIdentityForConversation(conversation, terminalControl));
1947
1743
  result.terminal_screen = result.terminal_status.screen;
1744
+ result.about = managedConversationAbout(conversation, events, result.terminal_status);
1745
+ result.limitations = result.terminal_status.reachable === false
1746
+ ? ["terminal status unavailable"]
1747
+ : [];
1748
+ }
1749
+ else {
1750
+ result.limitations = ["terminal control metadata is unavailable"];
1948
1751
  }
1949
1752
  printJson(result);
1950
1753
  runtimeLog("info", "task_status_read", {
@@ -1956,85 +1759,45 @@ async function runStatus(options) {
1956
1759
  trace: Boolean(options.trace)
1957
1760
  });
1958
1761
  }
1959
- async function runDescribe(options) {
1960
- cleanupIdleConversations(storeDirFromOptions(options), options);
1961
- const conversationId = required(options.conversation ?? options.conversationId, "--conversation is required");
1962
- const terminalConversation = await resolveTerminalConversationFromOptions(options);
1963
- if (terminalConversation) {
1964
- const terminalStatus = await terminalStatusForControl(terminalConversation.agent, terminalConversation.terminalControl, options, {
1965
- pid: terminalConversation.pid,
1966
- cwd: terminalConversation.terminalControl.currentPath,
1967
- conversationId: terminalConversation.conversationId,
1968
- terminalTarget: terminalConversation.terminalControl.target
1969
- });
1970
- if (terminalConversation.agent !== "codex") {
1971
- const adapter = createRuntimeTerminalAgentRegistry(options).require(terminalConversation.agent);
1972
- printJson({
1973
- conversation_id: conversationId,
1974
- source: "terminal_control",
1975
- agent: terminalConversation.agent,
1976
- confidence: terminalStatus.reachable ? "medium" : "low",
1977
- about: terminalStatus.reachable
1978
- ? `${adapter.displayName} is attached through ${terminalConversation.terminalControl.kind}:${terminalConversation.terminalControl.target}.`
1979
- : `${adapter.displayName} terminal status is unavailable.`,
1980
- evidence: {
1981
- terminal_status: terminalStatus,
1982
- terminal_screen: terminalStatus.screen
1983
- },
1984
- limitations: ["Historical session context is not available for this terminal adapter."],
1985
- terminal_control: terminalConversation.terminalControl
1762
+ async function terminalStatusContext(terminalConversation, terminalStatus, options) {
1763
+ if (terminalConversation.agent === "codex") {
1764
+ try {
1765
+ const process = await activeCodexProcessForPid(options, terminalConversation.pid);
1766
+ const description = await codexTerminalStatusContext({
1767
+ id: terminalConversation.conversationId,
1768
+ process,
1769
+ options,
1770
+ terminalControl: terminalConversation.terminalControl,
1771
+ terminalStatus
1986
1772
  });
1987
- return;
1773
+ return {
1774
+ confidence: description.confidence,
1775
+ about: description.about,
1776
+ limitations: description.limitations
1777
+ };
1778
+ }
1779
+ catch {
1780
+ return {
1781
+ confidence: "low",
1782
+ about: terminalStatus.reachable
1783
+ ? `Codex is attached through ${terminalConversation.terminalControl.kind}:${terminalConversation.terminalControl.target}.`
1784
+ : "Codex terminal status is unavailable.",
1785
+ limitations: [
1786
+ "Codex historical session context is unavailable; live terminal status remains authoritative."
1787
+ ]
1788
+ };
1988
1789
  }
1989
- const process = await activeCodexProcessForPid(options, terminalConversation.pid);
1990
- printJson(await describeNativeCodexSession({
1991
- id: conversationId,
1992
- source: "terminal_control",
1993
- process,
1994
- options,
1995
- terminalControl: terminalConversation.terminalControl,
1996
- terminalStatus
1997
- }));
1998
- return;
1999
- }
2000
- const nativeConversation = parseNativeConversationId(conversationId);
2001
- if (nativeConversation) {
2002
- const process = await activeCodexProcessForPid(options, nativeConversation.pid);
2003
- printJson(await describeNativeCodexSession({
2004
- id: conversationId,
2005
- source: "native_active",
2006
- process,
2007
- options
2008
- }));
2009
- return;
2010
1790
  }
2011
- const loaded = loadConversationFromOptions(options);
2012
- const { statePath, logPath } = loaded;
2013
- const conversation = await migrateLegacyTerminalAgentIdentity({
2014
- ...loaded,
2015
- options
2016
- });
2017
- const events = readExistingEvents(logPath);
2018
- const terminalControl = terminalControlFromTakeover(isRecord(conversation.native_session_takeover) ? conversation.native_session_takeover : undefined);
2019
- const terminalStatus = terminalControl
2020
- ? await terminalStatusForControl(executorForConversation(conversation).kind, terminalControl, options, terminalRuntimeIdentityForConversation(conversation, terminalControl))
2021
- : undefined;
2022
- printJson({
2023
- conversation_id: conversation.conversation_id,
2024
- source: "akk_managed",
2025
- confidence: "high",
2026
- about: managedConversationAbout(conversation, events, terminalStatus),
2027
- summary: summarizeConversation(conversation),
2028
- evidence: {
2029
- initial_request: conversation.user_request,
2030
- recent_messages: recentMessageEvidence(events),
2031
- trace: buildConversationTrace({ conversation, events, logPath }),
2032
- terminal_screen: terminalStatus?.screen
2033
- },
2034
- limitations: terminalStatus?.reachable === false ? ["terminal status unavailable"] : [],
2035
- state_path: statePath,
2036
- event_log_path: logPath
2037
- });
1791
+ const adapter = createRuntimeTerminalAgentRegistry(options).require(terminalConversation.agent);
1792
+ return {
1793
+ confidence: terminalStatus.reachable ? "medium" : "low",
1794
+ about: terminalStatus.reachable
1795
+ ? `${adapter.displayName} is attached through ${terminalConversation.terminalControl.kind}:${terminalConversation.terminalControl.target}.`
1796
+ : `${adapter.displayName} terminal status is unavailable.`,
1797
+ limitations: [
1798
+ "Historical session context is not available for this terminal adapter."
1799
+ ]
1800
+ };
2038
1801
  }
2039
1802
  async function terminalStatusForControl(agent, terminalControl, options, runtime) {
2040
1803
  return createTerminalAgentBridge(options).status(agent, terminalControl, {
@@ -3947,6 +3710,10 @@ async function runReconcileMonitors(options) {
3947
3710
  let skipped = 0;
3948
3711
  let errors = 0;
3949
3712
  for (const listedConversation of conversations) {
3713
+ if (!matchesConfiguredWorkspace(options.workspace, listedConversation.workspace)) {
3714
+ ignored += 1;
3715
+ continue;
3716
+ }
3950
3717
  const statePath = expandHome(stringValue(listedConversation.state_path) ??
3951
3718
  statePathForConversationId(listedConversation.conversation_id, storeDir));
3952
3719
  const logPath = expandHome(stringValue(listedConversation.event_log_path) ??
@@ -7558,6 +7325,7 @@ function loadConversationFromOptions(options) {
7558
7325
  const conversation = options.state
7559
7326
  ? loadState(statePath)
7560
7327
  : loadConversationById(conversationId, storeDir);
7328
+ assertConfiguredWorkspace(options.workspace, conversation.workspace, `access to AKK conversation ${conversation.conversation_id}`);
7561
7329
  return {
7562
7330
  conversation,
7563
7331
  statePath,
@@ -7609,7 +7377,7 @@ async function activeCodexProcessForPid(options, pid) {
7609
7377
  const activeSessions = await listActiveSessionsWithTerminalControl(provider, options);
7610
7378
  return activeSessions.find((process) => process.pid === pid);
7611
7379
  }
7612
- async function describeNativeCodexSession({ id, source, process, options, terminalControl, terminalStatus }) {
7380
+ async function codexTerminalStatusContext({ id, process, options, terminalControl, terminalStatus }) {
7613
7381
  const provider = createAgentSessionProvider("codex", options);
7614
7382
  const directSessionId = process?.sessionId;
7615
7383
  if (directSessionId) {
@@ -7620,9 +7388,8 @@ async function describeNativeCodexSession({ id, source, process, options, termin
7620
7388
  maxTextLength: Number(options.maxTextLength ?? 1200)
7621
7389
  });
7622
7390
  if (context) {
7623
- return nativeDescriptionFromContext({
7391
+ return codexTerminalContextFromHistory({
7624
7392
  id,
7625
- source,
7626
7393
  confidence: "high",
7627
7394
  match: "session_id",
7628
7395
  process,
@@ -7646,9 +7413,8 @@ async function describeNativeCodexSession({ id, source, process, options, termin
7646
7413
  maxTextLength: Number(options.maxTextLength ?? 1200)
7647
7414
  });
7648
7415
  if (context) {
7649
- return nativeDescriptionFromContext({
7416
+ return codexTerminalContextFromHistory({
7650
7417
  id,
7651
- source,
7652
7418
  confidence: sessions.length === 1 ? "medium" : "low",
7653
7419
  match: sessions.length === 1 ? "cwd" : "cwd_latest",
7654
7420
  process,
@@ -7670,7 +7436,7 @@ async function describeNativeCodexSession({ id, source, process, options, termin
7670
7436
  }
7671
7437
  return {
7672
7438
  conversation_id: id,
7673
- source,
7439
+ source: "terminal_control",
7674
7440
  confidence: "screen_only",
7675
7441
  match: "terminal_screen",
7676
7442
  about: screenOnlyAbout({ process, terminalStatus }),
@@ -7686,10 +7452,10 @@ async function describeNativeCodexSession({ id, source, process, options, termin
7686
7452
  ]
7687
7453
  };
7688
7454
  }
7689
- function nativeDescriptionFromContext({ id, source, confidence, match, process, context, terminalControl, terminalStatus, limitations, candidates }) {
7455
+ function codexTerminalContextFromHistory({ id, confidence, match, process, context, terminalControl, terminalStatus, limitations, candidates }) {
7690
7456
  return {
7691
7457
  conversation_id: id,
7692
- source,
7458
+ source: "terminal_control",
7693
7459
  confidence,
7694
7460
  match,
7695
7461
  about: rolloutAbout(context, terminalStatus),
@@ -8694,17 +8460,11 @@ function usage() {
8694
8460
  agent-knock-knock delegate --request <text> [--agent ${agentList}] [--workspace <path>] [--store-dir <dir>]
8695
8461
  agent-knock-knock list [--store-dir <dir>] [--agent ${agentList}] [--status <status>] [--all] [--managed-only] [--no-approval-scan] [--terminal-debug]
8696
8462
  agent-knock-knock status [--conversation <id|selector>] [--store-dir <dir>] [--trace]
8697
- agent-knock-knock describe [--conversation <id|selector>] [--store-dir <dir>]
8698
8463
  agent-knock-knock send [--conversation <id|selector>] --message <text> [--type answer|task|control] [--agent-timeout-minutes <minutes>] [--agent-hard-timeout-minutes <minutes>]
8699
- agent-knock-knock approve [--conversation <id|selector>]
8464
+ agent-knock-knock approve [--conversation <id|selector>] --expected-approval-fingerprint <fingerprint>
8700
8465
  agent-knock-knock cancel [--conversation <id|selector>]
8701
- agent-knock-knock renew [--conversation <id|selector>] [--minutes <inactivity-minutes>]
8702
- agent-knock-knock reconcile-monitors [--store-dir <dir>]
8703
- agent-knock-knock retry-callback --conversation <id> [--store-dir <dir>]
8704
- agent-knock-knock close --conversation <id> [--expected-message-id <id>] [--reason <text>]
8705
- agent-knock-knock install-openclaw [--workspace <path>] [--default-agent ${agentList}] [--verify] [--openclaw-bin <path>] [--skill-path <path>] [--skill-only] [--no-restart]
8466
+ agent-knock-knock install-openclaw [--workspace <path>] [--verify] [--openclaw-bin <path>] [--skill-path <path>] [--skill-only] [--no-restart]
8706
8467
  agent-knock-knock doctor [--workspace <path>] [--openclaw-bin <path>]
8707
- agent-knock-knock agent takeover --agent codex --session-id <id> --strategy terminal_control [--create-conversation]
8708
8468
  agent-knock-knock callback --state <file> --message-json <json> [--record-only]
8709
8469
  agent-knock-knock transcript --log <file> [--include-raw]
8710
8470
  agent-knock-knock transcript --conversation <dir> [--include-raw]