feat(implement-issue): add Section E to setup-skills for agent CLI selection

- Add agents-seed.md as source of truth for supported agents
- Add Section E to setup-skills to present agent menu and validate binary on PATH
- Refactor implement-issue to read docs/agents/agent-cli.md instead of hardcoded table
- Remove detect-agent-linux.sh and detect-agent-macos.sh detection scripts
- Add mise/$SHELL -c runtime detection for agent dispatch
- Add invocation-time binary validation with clear error message
- Add test file documenting expected tmux command composition

Closes #9
This commit is contained in:
2026-06-27 16:26:17 -04:00
parent fa29b93a02
commit f9611e1343
7 changed files with 342 additions and 131 deletions
+63 -34
View File
@@ -17,10 +17,22 @@ The issue tracker conventions live in [`docs/agents/issue-tracker.md`](../../../
```
- `<N>` — required, the issue number
- `--agent <name>` — optional, one of `pi`, `opencode`, `goose`, `codex`, `claude`. If omitted, auto-detected from the process tree.
- `--agent <name>` — **DEPRECATED**: this argument is ignored. The agent CLI is read from `docs/agents/agent-cli.md`, which is set up by `/setup-skills` Section E. Rerun `/setup-skills` to change the agent.
- `--base <branch>` — optional, target base branch (default: repo default branch).
- `--force` — optional, allow overwriting an existing worktree.
## Agent CLI Configuration
The CLI agent for child agents is configured in `docs/agents/agent-cli.md`. This file is written by `/setup-skills` Section E and contains:
```yaml
selected: <agent-name>
binary: <binary-name>
args: "<arguments including @{prompt}>"
```
Supported agents and their configurations are defined in `agents-seed.md` in the setup-skills skill directory.
## Process
### 1. Pre-flight checks
@@ -95,32 +107,27 @@ If it exists, report and stop. Let the user override with `--force`.
Confirm `$TMUX` is set. If not, report "This skill requires a tmux session. Start tmux and rerun." and stop.
#### 1i. Detect the agent binary
#### 1i. Read agent CLI config
If `--agent <name>` was provided in the invocation args, use that name directly. Otherwise, run the platform-appropriate detection script:
Read `docs/agents/agent-cli.md` and parse the `selected`, `binary`, and `args` fields. If the file is missing or malformed, report:
```bash
bash ./detect-agent-linux.sh
# or on macOS:
bash ./detect-agent-macos.sh
```
The script outputs the agent name on success (exit 0), or nothing on failure (exit 1).
If detection fails (no `--agent` flag and script returns nothing), report:
> Could not detect which agent is running. Rerun with `--agent <name>`.
> Supported agents: pi, opencode, goose, codex, claude.
> Agent CLI not configured. Run `/setup-skills` Section E to configure the CLI agent for child agents.
Stop.
If detection succeeds, confirm the binary is on PATH:
#### 1j. Validate binary on PATH
Confirm the configured `binary` is on PATH:
```bash
command -v <agent>
command -v <binary>
```
If the binary is not found, report "Agent binary '<agent>' not found on PATH." and stop.
If the binary is not found, fail with:
> Agent '<binary>' not found on PATH — rerun /setup-skills to reconfigure.
Stop.
### 2. Setup
@@ -173,27 +180,49 @@ PROMPT_EOF
### 4. Launch the child
Look up the full shell command from the dispatch table below, using the agent name detected in step 1i.
#### 4a. Detect runtime environment
| Agent | Shell command |
|-------|--------------|
| `pi` | `pi -p @/tmp/issue-<N>-prompt.md` |
| `opencode` | `opencode run -f /tmp/issue-<N>-prompt.md` |
| `goose` | `goose run -i /tmp/issue-<N>-prompt.md` |
| `codex` | `cat /tmp/issue-<N>-prompt.md \| codex exec` |
| `claude` | `cat /tmp/issue-<N>-prompt.md \| claude -p` |
| *unknown* | `cat /tmp/issue-<N>-prompt.md \| <binary>` |
Substitute the actual temp file path and issue number, then launch in a new tmux window:
Check if `mise` is available:
```bash
tmux new-window -n "issue-<N>-<repo>" -c <absolute-worktree-path> "<shell-command>"
command -v mise
```
For example, with pi:
If available, use `mise x --allow-env='*' --` as the runner prefix. Otherwise, fall back to `$SHELL -c`.
#### 4b. Compose the agent command
Substitute `@{prompt}` in the configured `args` with the absolute path to the temp prompt file:
- If `args` is non-empty: the agent command is `<binary> <substituted-args>`
- If `args` is empty (stdin-piping agents): the agent command is `cat <prompt-file> | <binary>`
#### 4c. Compose the full tmux command
```bash
tmux new-window -n "issue-42-myrepo" -c ../myrepo-issue-42 "pi -p @/tmp/issue-42-prompt.md"
tmux new-window -n "issue-<N>-<repo-slug>" -c <absolute-worktree-path> "<runner-prefix> <agent-command>"
```
**With mise:**
```bash
tmux new-window -n "issue-<N>-<repo>" -c /path/to/worktree "mise x --allow-env='*' -- <binary> <args-with-prompt-subbed>"
```
**Without mise (fallback):**
```bash
tmux new-window -n "issue-<N>-<repo>" -c /path/to/worktree "$SHELL -c '<binary> <args-with-prompt-subbed>'"
```
For stdin-piping agents (empty args), wrap the pipe command:
**With mise:**
```bash
tmux new-window -n "issue-<N>-<repo>" -c /path/to/worktree "mise x --allow-env='*' -- sh -c 'cat <prompt-file> | <binary>'"
```
**Without mise:**
```bash
tmux new-window -n "issue-<N>-<repo>" -c /path/to/worktree "$SHELL -c 'cat <prompt-file> | <binary>'"
```
The window stays open after the child completes so the user can review the output.
@@ -211,8 +240,8 @@ After launching, print:
- Worktree path
- Branch name
- Base branch
- Agent used
- Agent used (from `docs/agents/agent-cli.md`)
- Tmux window name (so the user can find it)
- Cleanup command: `git worktree remove <worktree-path> && git worktree prune`
The parent's turn ends here. The user can dispatch another issue immediately.
The parent's turn ends here. The user can dispatch another issue immediately.
@@ -0,0 +1,161 @@
# Agent CLI Tests for implement-issue
These tests verify the tmux command composition logic in `implement-issue`. They intercept the generated shell command string rather than the tmux invocation itself.
The seam being tested: given a `docs/agents/agent-cli.md` config and a `mise` availability state, `implement-issue` must compose the correct tmux launch command.
## Test fixtures
### Fixture: pi agent config (stdin-piped via @{prompt})
```yaml
# docs/agents/agent-cli.md
selected: pi
binary: pi
args: "-p @{prompt}"
```
### Fixture: opencode agent config
```yaml
# docs/agents/agent-cli.md
selected: opencode
binary: opencode
args: "run -f @{prompt}"
```
### Fixture: codex agent config (empty args = stdin pipe)
```yaml
# docs/agents/agent-cli.md
selected: codex
binary: codex
args: ""
```
### Fixture: missing binary config
```yaml
# docs/agents/agent-cli.md
selected: missing-agent
binary: totally-not-on-path
args: ""
```
## Test cases
### Test: composes correct tmux command for pi with mise available
**Given:**
- `docs/agents/agent-cli.md` contains the pi fixture
- `command -v mise` succeeds
**When:** `implement-issue` composes the tmux launch command
**Then:** the generated command string is:
```
tmux new-window -n "issue-42-ws-sjb-skills" -c /home/sjb/Projects/personal/ws-sjb-skills/ws-sjb-skills-issue-42 "mise x --allow-env='*' -- pi -p /tmp/issue-42-prompt.md"
```
**Notes:**
- `@{prompt}` is substituted with the actual temp file path
- Window name includes issue number and repo slug
- Worktree path is absolute
- `mise x --allow-env='*'` is prepended when mise is available
---
### Test: composes correct tmux command for pi without mise (fallback to $SHELL -c)
**Given:**
- `docs/agents/agent-cli.md` contains the pi fixture
- `command -v mise` fails (no mise on PATH)
**When:** `implement-issue` composes the tmux launch command
**Then:** the generated command string is:
```
tmux new-window -n "issue-42-ws-sjb-skills" -c /home/sjb/Projects/personal/ws-sjb-skills/ws-sjb-skills-issue-42 "$SHELL -c 'pi -p /tmp/issue-42-prompt.md'"
```
**Notes:**
- Falls back to `$SHELL -c` when mise is absent
- The agent command is wrapped inside `$SHELL -c '...'`
---
### Test: composes correct tmux command for codex (stdin piping, no @{prompt})
**Given:**
- `docs/agents/agent-cli.md` contains the codex fixture (args: "")
- `command -v mise` succeeds
**When:** `implement-issue` composes the tmux launch command
**Then:** the generated command string is:
```
tmux new-window -n "issue-42-ws-sjb-skills" -c /home/sjb/Projects/personal/ws-sjb-skills/ws-sjb-skills-issue-42 "mise x --allow-env='*' -- sh -c 'cat /tmp/issue-42-prompt.md | codex'"
```
**Notes:**
- When args is empty, prompt is piped via `cat <prompt> | <binary>`
- The pipe syntax is always `cat <prompt> | <binary>` for stdin-based agents
---
### Test: fails fast with clear message when binary not on PATH
**Given:**
- `docs/agents/agent-cli.md` contains the missing binary fixture
- `command -v totally-not-on-path` fails
**When:** `implement-issue` validates the binary at invocation time
**Then:** it fails immediately with:
```
Agent 'totally-not-on-path' not found on PATH — rerun /setup-skills to reconfigure.
```
**Notes:**
- Validation happens before any tmux launch
- Error message directs user to re-run setup-skills
---
### Test: reads selected agent from docs/agents/agent-cli.md
**Given:**
- `docs/agents/agent-cli.md` exists and contains valid YAML with `selected`, `binary`, and `args` fields
**When:** `implement-issue` reads the config at invocation time
**Then:** it parses the three fields correctly and uses them for dispatch
**Notes:**
- Does not use hardcoded dispatch table
- Does not run detection scripts
- Reads directly from the repo-side config file
---
### Test: @{prompt} placeholder substitution
**Given:**
- `docs/agents/agent-cli.md` contains opencode fixture (args: "run -f @{prompt}")
- `command -v mise` succeeds
**When:** `implement-issue` composes the tmux launch command with prompt file at `/tmp/issue-99-prompt.md`
**Then:** the generated command contains:
```
opencode run -f /tmp/issue-99-prompt.md
```
**Notes:**
- `@{prompt}` in args is replaced with the absolute path to the temp prompt file
- Works regardless of where `@{prompt}` appears in the args string
@@ -1,44 +0,0 @@
#!/usr/bin/env bash
# Walk the process tree upward from $PPID to find a known agent binary.
# Outputs the agent name on stdout if found, exits 0.
# Outputs nothing and exits 1 if no known agent is found.
set -euo pipefail
# Dispatch table: binary names we recognize.
KNOWN_AGENTS="pi opencode goose codex claude"
pid=$PPID
while [ "$pid" -gt 1 ]; do
# Read command name and parent PID from /proc.
comm=$(cat /proc/$pid/comm 2>/dev/null) || { pid=1; continue; }
ppid=$(awk '{print $4}' /proc/$pid/stat 2>/dev/null) || ppid=1
# Direct match against known binaries.
for agent in $KNOWN_AGENTS; do
if [ "$comm" = "$agent" ]; then
echo "$agent"
exit 0
fi
done
# If the process is node or bun, inspect cmdline for agent script paths.
case "$comm" in
node|bun)
cmdline=$(tr '\0' ' ' < /proc/$pid/cmdline 2>/dev/null) || cmdline=""
for agent in $KNOWN_AGENTS; do
case "$cmdline" in
*"$agent"*)
echo "$agent"
exit 0
;;
esac
done
;;
esac
pid=$ppid
done
exit 1
@@ -1,48 +0,0 @@
#!/usr/bin/env bash
# Walk the process tree upward from $PPID to find a known agent binary.
# Outputs the agent name on stdout if found, exits 0.
# Outputs nothing and exits 1 if no known agent is found.
#
# macOS version — uses ps(1) since /proc is not available.
# Requires bash 3.2+ (macOS ships bash 3.2).
set -euo pipefail
KNOWN_AGENTS="pi opencode goose codex claude"
pid=$PPID
while [ "$pid" -gt 1 ]; do
# Get parent PID and command name.
ppid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ') || ppid=1
comm=$(ps -o comm= -p "$pid" 2>/dev/null) || { pid=$ppid; continue; }
# Strip leading path, keep only the basename.
comm=$(basename "$comm" 2>/dev/null || echo "$comm")
# Direct match against known binaries.
for agent in $KNOWN_AGENTS; do
if [ "$comm" = "$agent" ]; then
echo "$agent"
exit 0
fi
done
# If the process is node or bun, inspect full command line for agent names.
case "$comm" in
node|bun)
args=$(ps -o args= -p "$pid" 2>/dev/null) || args=""
for agent in $KNOWN_AGENTS; do
case "$args" in
*"$agent"*)
echo "$agent"
exit 0
;;
esac
done
;;
esac
pid=$ppid
done
exit 1