mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
feat(adopt): adopt tmux sessions a human started outside Codeman
The home screen now lists tmux sessions Codeman did not start (a claude or codex running inside `tmux new -s work`, or just a shell); one click turns one into a tab you can keep working in. Adoption is a fourth LOCATION OVERLAY, structurally identical to remote/docker, and NOT a new SessionMode: the outer layer is still an ordinary codeman-<8hex> wrapper session on this instance's own socket, and only the pane inside it runs the attach. Session-name allowlisting, capture, input and the recovery chain are therefore untouched, and "detach, never kill the foreign session" becomes structural rather than a rule to remember — killSession can only ever reach our own wrapper. All three locations share one probe script, one parser and one classifier, and differ only in the shell around them (direct exec / docker exec / ssh). Pane mode is decided from the bounded process-tree argv of pane_pid, because claude and codex both report `node` as pane_current_command; anything unrecognised is treated as a shell. Things measured rather than assumed: - A grouped session buys only `status off` and an independent current window, not an independent size. Measured on tmux 3.3a: both a bare attach and a grouped one shrink the other client's 200x49 to 80x23. Only `window-size largest` preserves it, but that is a shared window option that survives our departure, so it is not set. - View reclamation: local relies on client death, ssh on SIGHUP, but a `docker exec` does not die with its client — the container-side view must be reclaimed explicitly on kill or every adoption leaks one. - The session name is chosen by someone else, while the local launch chain ends in `bash -c` plus JSON.stringify, which does not escape `$` or backticks, so the outer shell performs substitution before the inner single quotes close. Session names and socket paths therefore pass a character allowlist and are discarded during DISCOVERY, so a non-conforming candidate never gets an id. Capability degrades by "who started this process": an adopted session has no hooks, no envOverrides and no effort, and its working directory is merely the foreign pane's cwd at that moment (possibly not even on this host). Respawn, Ralph, the orchestrator, hook waits, and every watcher that tails the local filesystem by workingDir are refused or skipped, and the close dialog no longer offers a "kill the session" option it cannot honour.
This commit is contained in:
@@ -189,6 +189,18 @@ export interface HookCapabilityOptions {
|
||||
* timeout on every turn.
|
||||
*/
|
||||
deepSeekBridgeUnreachable?: boolean;
|
||||
/**
|
||||
* True when the session is a WRAPPER around a tmux session a human started
|
||||
* outside Codeman.
|
||||
*
|
||||
* This one overrides the mode entirely, and it has to: an adopted session can
|
||||
* be `mode: 'claude'` and still have no hooks, because hooks are installed into
|
||||
* a WORKSPACE at session-create time (`applyWorkspaceHooks`) and we never
|
||||
* created this one. Answering from the mode there would promise `stop` and
|
||||
* `blocked` for a process that can never post either — the exact
|
||||
* infinite-wait-dressed-as-a-timeout this predicate exists to prevent.
|
||||
*/
|
||||
adopted?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,6 +235,9 @@ export interface HookCapabilityOptions {
|
||||
* function only about hook SIGNALS.
|
||||
*/
|
||||
export function hooksAvailableForMode(mode: SessionMode, options: HookCapabilityOptions = {}): boolean {
|
||||
// Checked BEFORE the mode: adoption is about who launched the process, and no
|
||||
// mode can vouch for a workspace Codeman never touched. See `adopted` above.
|
||||
if (options.adopted) return false;
|
||||
if (mode === 'claude') return true;
|
||||
// `deepseek` earns this the same way `claude` does — by emitting DEFINITIVE
|
||||
// signals rather than having them inferred. The DeepSeek Harness terminal
|
||||
@@ -250,10 +265,12 @@ export function sessionHookOptions(session: {
|
||||
deepSeekStatusReporting?: boolean;
|
||||
docker?: unknown;
|
||||
remote?: unknown;
|
||||
adopt?: unknown;
|
||||
}): HookCapabilityOptions {
|
||||
return {
|
||||
deepSeekStatusReporting: session.deepSeekStatusReporting,
|
||||
deepSeekBridgeUnreachable: Boolean(session.docker || session.remote),
|
||||
adopted: Boolean(session.adopt),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user