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:
@@ -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
|
||||
Reference in New Issue
Block a user