refactor: extract tmux-launch-agent from implement-issue/setup-skills, overhaul PKM skills
- New common/misc/tmux-launch-agent skill with detect-agent script and agents-seed.md (moved from engineering/setup-skills) - implement-issue simplified: delegates agent dispatch to tmux-launch-agent instead of inlining it - setup-skills stripped of Section E (agent CLI config) - New in-progress/agent-handoff skill using tmux-launch-agent - PKM skills simplified: conversation-summary and pkm-curation rewritten with leaner process, crit streamlined - knowledge-gardener removed (moved to common/in-progress/) - All READMEs updated to match
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: tmux-launch-agent
|
||||
description: Fork a new agent CLI session into a new tmux window, detected from the current agent.
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
## Agent CLI Seed Data
|
||||
|
||||
The agent config (binary, `args` convention per agent) and field meanings are in [`agents-seed.md`](agents-seed.md). Step 2 reads it to find the calling agent's entry.
|
||||
|
||||
|
||||
## Process
|
||||
|
||||
1. **Detect the calling agent** — Run `./detect-agent` (sibling to this skill). If it exits 1 (agent unknown), report the failure and stop — the agent name is required.
|
||||
|
||||
Completion criterion: The agent name is known and non-empty.
|
||||
|
||||
2. **Look up the agent config** — Find the agent's entry in [`agents-seed.md`](agents-seed.md) by `name`. Extract its `binary` and `args` fields.
|
||||
|
||||
Completion criterion: The agent's entry is found and its `binary` and `args` are known.
|
||||
|
||||
3. **Parse flags** — Scan the user's arguments for optional flags (which must come before the prompt text):
|
||||
- `--name <title>` or `-n <title>` — the tmux window title. Extract the title and remove the flag and its value from the arguments list.
|
||||
- `-c <path>` — the working directory for the new window. Extract the path and remove the flag and its value from the arguments list.
|
||||
|
||||
The remaining text after stripping both flags is the prompt for the child agent.
|
||||
|
||||
If `--name`/`-n` is absent, tmux auto-names the window.
|
||||
If `-c` is absent, the new window inherits the current pane's working directory.
|
||||
|
||||
Completion criterion: The arguments are split into an optional window name, an optional directory path, and the remaining prompt text.
|
||||
|
||||
4. **Build the inner command** — Combine the agent config with the remaining user-supplied prompt arguments. The `args` template determines how the prompt is delivered:
|
||||
|
||||
- **Prompt-file agents** (`args` contains `{prompt}`) — Write the prompt text to a temporary file under `/tmp/` and substitute the file path for `{prompt}` in the args template. For example, an agent with `args: "@{prompt}"` becomes `<binary> @/tmp/tmux-launch-XXXX.md`.
|
||||
- **Stdin-pipe agents** (`args` is empty) — Pipe the prompt text via `echo` into the binary.
|
||||
- **No prompt** — If the user passed no arguments (after removing flags), launch the binary bare (interactive start) with no prompt file or pipe.
|
||||
|
||||
Completion criterion: The inner command is correctly built per the target agent's `args` convention (prompt-file, stdin-pipe, or bare).
|
||||
|
||||
5. **Wrap with the environment runner** — Detect whether `mise` is available via `command -v mise`:
|
||||
|
||||
- **mise available** — Wrap the inner command as `mise x --allow-env='*' -- <inner-command>`.
|
||||
- **mise absent** — Wrap the inner command as `$SHELL -c '<inner-command>'`.
|
||||
|
||||
Completion criterion: A valid shell command string is ready.
|
||||
|
||||
6. **Fork into a new tmux window** — Build and run:
|
||||
```
|
||||
tmux new-window <name-flag> <dir-flag> "<shell-command>"
|
||||
```
|
||||
- `<shell-command>` — the wrapped command from step 5.
|
||||
- `<name-flag>` — `-n "<title>"` if a window name was parsed in step 3, omitted otherwise.
|
||||
- `<dir-flag>` — `-c <path>` if a directory was parsed in step 3, omitted otherwise.
|
||||
|
||||
Completion criterion: `tmux new-window` exits 0 and a new tmux window appears with the agent CLI session active.
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# TMUX Launch Agent — Agent CLI Seed Data
|
||||
|
||||
## Agents
|
||||
|
||||
```yaml
|
||||
agents:
|
||||
- name: pi
|
||||
binary: pi
|
||||
args: "@{prompt}"
|
||||
description: "My primary agent harness. Accepts prompt file via {prompt}."
|
||||
|
||||
- name: opencode
|
||||
binary: opencode
|
||||
args: ""
|
||||
description: "OpenCode agent. Pipes stdin via cat."
|
||||
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
||||
|
||||
- name: goose
|
||||
binary: goose
|
||||
args: ""
|
||||
description: "Goose agent. Accepts prompt file via -i flag."
|
||||
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
||||
|
||||
- name: codex
|
||||
binary: codex
|
||||
args: "@{prompt}"
|
||||
description: "OpenAI Codex. Accepts prompt file via {prompt}."
|
||||
|
||||
- name: claude
|
||||
binary: claude
|
||||
args: ""
|
||||
description: "Anthropic Claude CLI. Pipes stdin via cat."
|
||||
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
||||
```
|
||||
|
||||
## Field meanings
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `name` | Display name used in menus and `--agent` flag |
|
||||
| `binary` | Command name expected on PATH |
|
||||
| `args` | Static arguments appended after the binary. May include `{prompt}` which is substituted at invocation time with the absolute path to the prompt file. Empty string means stdin piping (prompt is piped via `cat`). |
|
||||
| `description` | Short human-readable description for the setup menu |
|
||||
| `note` | Optional additional context |
|
||||
Executable
+353
@@ -0,0 +1,353 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# detect-agent — Detect which AI agent CLI launched this script.
|
||||
#
|
||||
# Checks environment variables and walks the parent process tree to
|
||||
# identify the originating coding agent. Designed for agent-agnostic
|
||||
# tools (skills, scripts, hooks) that need to adjust behaviour based
|
||||
# on which agent invoked them.
|
||||
#
|
||||
# Usage:
|
||||
# detect-agent # print name (e.g. "pi", "opencode")
|
||||
# detect-agent --name # same as default
|
||||
# detect-agent --json # JSON: {"agent":"pi","detected":true}
|
||||
# detect-agent --verbose # human-readable with debug info
|
||||
# detect-agent --list-agents # list all known agent names
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 agent detected
|
||||
# 1 unknown / not detected
|
||||
#
|
||||
# Supported agents (add your own at the top of each definition map):
|
||||
# pi, opencode, aider, claude, codex, cursor, windsurf, continue,
|
||||
# github-copilot, goose
|
||||
#
|
||||
# Detection order:
|
||||
# 1. Environment variable signatures (most reliable)
|
||||
# 2. Walk parent process tree skipping shells
|
||||
# 3. Check /proc/$PPID/{comm,cmdline} as fallback
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
########################################################################
|
||||
# Agent definitions
|
||||
########################################################################
|
||||
|
||||
# Each entry: "<env-var>:<match-value>:<agent-name>"
|
||||
# If match-value is "*", any non-empty value matches.
|
||||
# If match-value is a literal, the env var must equal it.
|
||||
AGENT_ENV_SIGS=(
|
||||
"PI_CODING_AGENT:true:pi"
|
||||
"OPENCODE_CONFIG:*:opencode"
|
||||
"OPENCODE_CONFIG_DIR:*:opencode"
|
||||
"OPENCODE_API_KEY:*:opencode"
|
||||
"CLAUDE_CODE_AGENT_RULE_DISABLED:*:claude"
|
||||
"CLAUDE_CODE_SSE_PORT:*:claude"
|
||||
"AIDER_DARK_MODE:*:aider"
|
||||
"CODEX_API_KEY:*:codex"
|
||||
"CURSOR_AGENT_RULE_DISABLED:*:cursor"
|
||||
"WINDSURF_AGENT_RULE_DISABLED:*:windsurf"
|
||||
"CONTINUE_ON_ERROR:*:continue"
|
||||
)
|
||||
|
||||
# Key = /proc/PID/comm (truncated to 15 chars on Linux).
|
||||
# Value = canonical agent name.
|
||||
declare -A AGENT_COMM
|
||||
AGENT_COMM=(
|
||||
[pi]="pi"
|
||||
[opencode]="opencode"
|
||||
[aider]="aider"
|
||||
[claude]="claude"
|
||||
[codex]="codex"
|
||||
[cursor]="cursor"
|
||||
[windsurf]="windsurf"
|
||||
[continue]="continue"
|
||||
[github-copilot]="github-copilot"
|
||||
[copilot]="github-copilot"
|
||||
[goose]="goose"
|
||||
)
|
||||
|
||||
# For agents running under an interpreter (node, python, etc.).
|
||||
# Key = substring to search in cmdline, Value = agent name.
|
||||
declare -A AGENT_CMDLINE
|
||||
AGENT_CMDLINE=(
|
||||
["opencode"]="opencode"
|
||||
["/pi "]="pi"
|
||||
["/aider"]="aider"
|
||||
["/claude"]="claude"
|
||||
["/codex"]="codex"
|
||||
["cursor"]="cursor"
|
||||
["windsurf"]="windsurf"
|
||||
["continue.dev"]="continue"
|
||||
["github-copilot"]="github-copilot"
|
||||
["goose"]="goose"
|
||||
)
|
||||
|
||||
# Shells to skip when walking ancestors.
|
||||
SHELL_COMMS=(
|
||||
bash zsh fish sh dash ksh mksh posh ash tcsh csh
|
||||
tmux:server tmux screen
|
||||
sudo doas
|
||||
)
|
||||
|
||||
SHELL_CMDLINE_SUBSTRINGS=(
|
||||
"/bash" "/zsh" "/fish" "/sh" "/dash" "/ksh"
|
||||
)
|
||||
|
||||
########################################################################
|
||||
# Helpers
|
||||
########################################################################
|
||||
|
||||
comm_of() {
|
||||
local pid="$1"
|
||||
cat "/proc/${pid}/comm" 2>/dev/null || true
|
||||
}
|
||||
|
||||
cmdline_of() {
|
||||
local pid="$1"
|
||||
tr '\0' ' ' <"/proc/${pid}/cmdline" 2>/dev/null || true
|
||||
}
|
||||
|
||||
is_shell() {
|
||||
local pid="$1"
|
||||
local c; c="$(comm_of "$pid")" || true
|
||||
[[ -z "$c" ]] && return 1
|
||||
|
||||
for s in "${SHELL_COMMS[@]}"; do
|
||||
[[ "$c" == "$s" ]] && return 0
|
||||
done
|
||||
|
||||
local cl; cl="$(cmdline_of "$pid")" || true
|
||||
for s in "${SHELL_CMDLINE_SUBSTRINGS[@]}"; do
|
||||
[[ "$cl" == *"$s"* ]] && return 0
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
walk_to_agent_comm() {
|
||||
local pid="$1"
|
||||
local c
|
||||
while [[ "$pid" -gt 1 ]]; do
|
||||
c="$(comm_of "$pid")" || true
|
||||
[[ -z "$c" ]] && break
|
||||
if ! is_shell "$pid"; then
|
||||
echo "$c"
|
||||
return 0
|
||||
fi
|
||||
pid="$(awk '/^PPid:/{print $2}' "/proc/${pid}/status" 2>/dev/null || true)"
|
||||
[[ -z "$pid" ]] && break
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
walk_to_agent_cmdline() {
|
||||
local pid="$1"
|
||||
local cl
|
||||
while [[ "$pid" -gt 1 ]]; do
|
||||
if ! is_shell "$pid"; then
|
||||
cl="$(cmdline_of "$pid")" || true
|
||||
if [[ -n "$cl" ]]; then
|
||||
echo "$cl"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
pid="$(awk '/^PPid:/{print $2}' "/proc/${pid}/status" 2>/dev/null || true)"
|
||||
[[ -z "$pid" ]] && break
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
check_env_sigs() {
|
||||
local var val agent
|
||||
for sig in "${AGENT_ENV_SIGS[@]}"; do
|
||||
IFS=':' read -r var val agent <<< "$sig"
|
||||
if [[ "$val" == "*" ]]; then
|
||||
if [[ -n "${!var:-}" ]]; then
|
||||
echo "$agent"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
if [[ "${!var:-}" == "$val" ]]; then
|
||||
echo "$agent"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
match_cmdline() {
|
||||
local cl="$1"
|
||||
local needle agent
|
||||
for needle in "${!AGENT_CMDLINE[@]}"; do
|
||||
agent="${AGENT_CMDLINE[$needle]}"
|
||||
if [[ "$cl" == *"$needle"* ]]; then
|
||||
echo "$agent"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
match_comm() {
|
||||
local c="$1"
|
||||
local comm_name
|
||||
for comm_name in "${!AGENT_COMM[@]}"; do
|
||||
if [[ "$c" == "$comm_name" ]]; then
|
||||
echo "${AGENT_COMM[$comm_name]}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
########################################################################
|
||||
# Main detection logic
|
||||
########################################################################
|
||||
|
||||
detect() {
|
||||
local agent=""
|
||||
|
||||
# Phase 1 — environment variables (most reliable)
|
||||
agent="$(check_env_sigs)" || true
|
||||
[[ -n "$agent" ]] && echo "$agent" && return 0
|
||||
|
||||
# Phase 2 — walk ancestors, check comm
|
||||
local ancestor_comm
|
||||
ancestor_comm="$(walk_to_agent_comm "$PPID")" || true
|
||||
if [[ -n "$ancestor_comm" ]]; then
|
||||
agent="$(match_comm "$ancestor_comm")" || true
|
||||
[[ -n "$agent" ]] && echo "$agent" && return 0
|
||||
|
||||
# comm matched but not in our map; try its cmdline
|
||||
local ancestor_pid="$PPID"
|
||||
while [[ "$ancestor_pid" -gt 1 ]]; do
|
||||
local ac; ac="$(comm_of "$ancestor_pid")" || true
|
||||
if [[ "$ac" == "$ancestor_comm" ]]; then
|
||||
local ancestor_cl
|
||||
ancestor_cl="$(cmdline_of "$ancestor_pid")" || true
|
||||
if [[ -n "$ancestor_cl" ]]; then
|
||||
agent="$(match_cmdline "$ancestor_cl")" || true
|
||||
[[ -n "$agent" ]] && echo "$agent" && return 0
|
||||
fi
|
||||
break
|
||||
fi
|
||||
ancestor_pid="$(awk '/^PPid:/{print $2}' "/proc/${ancestor_pid}/status" 2>/dev/null || true)"
|
||||
done
|
||||
fi
|
||||
|
||||
# Phase 3 — walk ancestors, check cmdline (catches node-based agents)
|
||||
local ancestor_cl
|
||||
ancestor_cl="$(walk_to_agent_cmdline "$PPID")" || true
|
||||
if [[ -n "$ancestor_cl" ]]; then
|
||||
agent="$(match_cmdline "$ancestor_cl")" || true
|
||||
[[ -n "$agent" ]] && echo "$agent" && return 0
|
||||
fi
|
||||
|
||||
# Phase 4 — fallback: check immediate PPID directly
|
||||
local ppid_comm; ppid_comm="$(comm_of "$PPID")" || true
|
||||
agent="$(match_comm "$ppid_comm")" || true
|
||||
[[ -n "$agent" ]] && echo "$agent" && return 0
|
||||
|
||||
local ppid_cl; ppid_cl="$(cmdline_of "$PPID")" || true
|
||||
agent="$(match_cmdline "$ppid_cl")" || true
|
||||
[[ -n "$agent" ]] && echo "$agent" && return 0
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
########################################################################
|
||||
# Output
|
||||
########################################################################
|
||||
|
||||
mode="${1:---name}"
|
||||
|
||||
case "$mode" in
|
||||
--list-agents)
|
||||
echo "Known agents (by env-var / process-name):"
|
||||
echo ""
|
||||
echo "Environment variable signatures:"
|
||||
for sig in "${AGENT_ENV_SIGS[@]}"; do
|
||||
IFS=':' read -r var val agent <<< "$sig"
|
||||
printf " %-12s %-8s %s=%s\n" "$agent" "(env)" "$var" "$val"
|
||||
done
|
||||
echo ""
|
||||
echo "Process comm signatures:"
|
||||
for comm_name in "${!AGENT_COMM[@]}"; do
|
||||
printf " %-12s %-8s %s\n" "${AGENT_COMM[$comm_name]}" "(comm)" "$comm_name"
|
||||
done
|
||||
echo ""
|
||||
echo "Cmdline substring signatures:"
|
||||
for needle in "${!AGENT_CMDLINE[@]}"; do
|
||||
printf " %-12s %-8s *%s*\n" "${AGENT_CMDLINE[$needle]}" "(cmdline)" "$needle"
|
||||
done
|
||||
;;
|
||||
|
||||
--json)
|
||||
agent="$(detect)" || agent="null"
|
||||
if [[ "$agent" != "null" ]]; then
|
||||
printf '{"agent":"%s","detected":true}\n' "$agent"
|
||||
else
|
||||
printf '{"agent":null,"detected":false}\n'
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
--verbose)
|
||||
echo "=== Agent Detection ==="
|
||||
agent="$(detect)" || true
|
||||
if [[ -n "$agent" ]]; then
|
||||
echo "Detected agent: $agent"
|
||||
else
|
||||
echo "Detected agent: (unknown)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "Relevant environment variables:"
|
||||
for sig in "${AGENT_ENV_SIGS[@]}"; do
|
||||
IFS=':' read -r var val _ <<< "$sig"
|
||||
# Skip entries that aren't real variable names (e.g. patterns with *).
|
||||
[[ "$var" == *\** ]] && continue
|
||||
if [[ -n "${!var:-}" ]]; then
|
||||
echo " $var=${!var}"
|
||||
fi
|
||||
done
|
||||
[[ -z "${PI_CODING_AGENT:-}" ]] && echo " PI_CODING_AGENT=<unset>"
|
||||
echo ""
|
||||
|
||||
echo "Process tree (up from PPID=$PPID):"
|
||||
_dbg_pid="$PPID"
|
||||
_dbg_depth=0
|
||||
while [[ "$_dbg_pid" -gt 1 && "$_dbg_depth" -lt 15 ]]; do
|
||||
_dbg_c="$(comm_of "$_dbg_pid")" || true
|
||||
_dbg_cl="$(cmdline_of "$_dbg_pid")" || true
|
||||
if [[ -n "$_dbg_c" ]]; then
|
||||
printf " [%d] %s" "$_dbg_depth" "$_dbg_c"
|
||||
if [[ -n "$_dbg_cl" ]]; then
|
||||
printf " <- %s" "${_dbg_cl:0:100}"
|
||||
fi
|
||||
if [[ -n "$agent" ]]; then
|
||||
_dbg_mc="$(match_comm "$_dbg_c")" || true
|
||||
if [[ "$_dbg_mc" == "$agent" ]]; then
|
||||
printf " <-- DETECTED"
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
_dbg_pid="$(awk '/^PPid:/{print $2}' "/proc/${_dbg_pid}/status" 2>/dev/null || true)"
|
||||
_dbg_depth=$((_dbg_depth + 1))
|
||||
done
|
||||
;;
|
||||
|
||||
--name|*)
|
||||
agent="$(detect)" || agent=""
|
||||
if [[ -n "$agent" ]]; then
|
||||
echo "$agent"
|
||||
else
|
||||
echo "unknown"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user