docs(issue-42): stages 1-2 - SKILL.md code-block inventory as test data; remove blocks the scripts already do
Stage 1: tests/data/skill_code_blocks.json lists all 37 fenced blocks of the
eight SKILL.md files at base 45d1621 (indented fences count), classed (a)
duplicate of a script, (b) procedure no script offers, (c) example/call/
diagram, with script file:line evidence and recorded mismatches.
tests/test_issue42_skill_blocks.py fails when a block changes without the
inventory, an (a) block comes back, a removed block's entry call is missing,
an exit-code table drifts from the script, or evidence points nowhere.
Stage 2: the five (a) blocks are gone - resume Workflow, stop pre-flight,
create pre-flight / Workflow / manual YAML append - replaced by the
entry-point call with option and exit-code tables. Each exit table has a
catch-all row: under set -e a failing sub-step's code passes through.
create's exit 4 is real (the YAML write raises SystemExit(4) for a duplicate
running row) and is listed as such. Doc-to-script mismatches are fixed in the
doc (resume Case 3 / no attach, stop --purge-conversation --yes, status
--json keys, create cmd_full and the nonexistent yaml-edit script). No
behaviour change, no version bump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
45d1621c93
commit
7670b41afd
6 files changed
+901
-226
No files matched your search
@@ -30,28 +30,9 @@ So `$WORKSPACE_ROOT/landing_page/refer_landing_page` + `claude` → `landing-pag
|
|||||||
|
|
||||||
## Pre-flight checks
|
## Pre-flight checks
|
||||||
|
|
||||||
Before doing anything, verify the environment:
|
`create_session.sh` checks the environment itself and exits 1 if anything is missing: herdr installed (`has_real_herdr`), the agent CLI in `PATH`, the agent's authentication, and the workspace directory. In your own scripts, test for herdr with lib.sh's `has_real_herdr`, NOT `command -v herdr` / `type -P herdr`: once lib.sh is sourced the former matches its `herdr()` function and the latter matches the `.mam/shim` wrapper, so both pass on a host with no herdr (B-3).
|
||||||
|
|
||||||
```bash
|
If a check fails, report the reason (automated path) or tell the user (interactive path). Do not proceed with a half-broken setup.
|
||||||
# 1) herdr available and isolated server status
|
|
||||||
# Use lib.sh's has_real_herdr, NOT `command -v herdr` / `type -P herdr`: once
|
|
||||||
# lib.sh is sourced the former matches its herdr() function and the latter
|
|
||||||
# matches the .mam/shim wrapper, so both pass on a host with no herdr (B-3).
|
|
||||||
has_real_herdr || { echo "ERROR: herdr not installed"; exit 1; }
|
|
||||||
echo "Herdr session name: ${HERDR_SESSION_NAME:-default}"
|
|
||||||
|
|
||||||
# 2) claude / agy available
|
|
||||||
command -v claude # required for --agent claude
|
|
||||||
command -v agy # required for --agent agy
|
|
||||||
|
|
||||||
# 3) claude auth (if --agent claude)
|
|
||||||
claude auth status 2>&1 | python3 -c "import json,sys; d=json.load(sys.stdin); assert d.get('loggedIn'), 'claude not logged in'"
|
|
||||||
|
|
||||||
# 4) target workspace exists
|
|
||||||
test -d "$WORKSPACE" || { echo "ERROR: workspace $WORKSPACE not a directory"; exit 1; }
|
|
||||||
```
|
|
||||||
|
|
||||||
If any check fails → abort with a non-zero exit and report the reason (automated path) or report to user (interactive path). Do not proceed with a half-broken setup.
|
|
||||||
|
|
||||||
## Standard names
|
## Standard names
|
||||||
|
|
||||||
@@ -163,109 +144,45 @@ herdr_sessions:
|
|||||||
kill_command: "HERDR_SESSION_NAME=<herdr_session> herdr kill-session -t <SESSION_NAME>"
|
kill_command: "HERDR_SESSION_NAME=<herdr_session> herdr kill-session -t <SESSION_NAME>"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The three commands require `source .agents/skills/lib.sh` first — `new-session`/`kill-session` are tmux-compat pseudo-commands the shim translates, and `HERDR_SESSION_NAME` is what the shim reads to route to the right isolated herdr *session* (real `herdr` has no env-var-based scoping of its own; `herdr_session: default` needs no prefix at all).
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
WORKSPACE=/path/to/project
|
bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh --workspace <path> --agent <agent> --role <role>
|
||||||
AGENT=claude # claude | agy | hermes | grok | opencode | codex — always pass it explicitly
|
|
||||||
source .agents/skills/lib.sh
|
|
||||||
SESSION_NAME="$(derive_session_name "$WORKSPACE" "$AGENT")"
|
|
||||||
|
|
||||||
# 1. If session already alive, fail fast
|
|
||||||
herdr has-session -t "$SESSION_NAME" 2>/dev/null && {
|
|
||||||
echo "ERROR: herdr session '$SESSION_NAME' already exists. Use multi-agent-mux-resume to attach or multi-agent-mux-stop first."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 2. Spawn the herdr session with the agent inside
|
|
||||||
case "$AGENT" in
|
|
||||||
claude)
|
|
||||||
# Use the wrapper if it exists, else inline herdr new-session
|
|
||||||
# Use the wrapper if it exists (LOCAL_BIN env var overrides default $HOME/.local/bin)
|
|
||||||
local_bin="${LOCAL_BIN:-$HOME/.local/bin}"
|
|
||||||
if [ -x "$local_bin/$SESSION_NAME" ]; then
|
|
||||||
nohup "$local_bin/$SESSION_NAME" >/dev/null 2>&1 &
|
|
||||||
else
|
|
||||||
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "claude"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
agy)
|
|
||||||
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "agy --dangerously-skip-permissions"
|
|
||||||
;;
|
|
||||||
hermes)
|
|
||||||
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "hermes --yolo --accept-hooks"
|
|
||||||
;;
|
|
||||||
grok)
|
|
||||||
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "grok --permission-mode bypassPermissions"
|
|
||||||
;;
|
|
||||||
opencode)
|
|
||||||
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "opencode --auto --agent build"
|
|
||||||
;;
|
|
||||||
codex)
|
|
||||||
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "codex --dangerously-bypass-approvals-and-sandbox"
|
|
||||||
;;
|
|
||||||
*) echo "ERROR: --agent must be claude, agy, hermes, grok, opencode, or codex, got: $AGENT"; exit 2 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# 3. Wait for agent TUI to be ready (varies: claude ~5s, agy ~3s)
|
|
||||||
sleep 6
|
|
||||||
|
|
||||||
# 4. Capture pane metadata
|
|
||||||
PANE_PID=$(herdr list-panes -t "$SESSION_NAME" -F '#{pane_pid}')
|
|
||||||
PANE_CWD=$(herdr list-panes -t "$SESSION_NAME" -F '#{pane_current_path}')
|
|
||||||
PANE_CMD=$(herdr list-panes -t "$SESSION_NAME" -F '#{pane_current_command}')
|
|
||||||
# `herdr list-sessions` doesn't exist (real or shimmed) — we just spawned this
|
|
||||||
# session ourselves, so stamp the epoch locally instead of round-tripping herdr.
|
|
||||||
HERDR_EPOCH=$(date +%s)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`create_session.sh` does the whole flow: pre-flight, the session name (`derive_session_name`), the agent command (adapter `spawn-spec`), spawn, start-up dialogs, waiting for the TUI, pane capture, the YAML row, and the optional job submission.
|
||||||
|
|
||||||
|
| Option | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `--workspace` | Required. Project directory |
|
||||||
|
| `--agent` | Required. `claude`, `agy`, `hermes`, `grok`, `opencode`, `codex` |
|
||||||
|
| `--role` | Required. Assigned role |
|
||||||
|
| `--session` (`--name`) | herdr session name (default: `derive_session_name`) |
|
||||||
|
| `--wrapper` | claude only: use the `~/.local/bin/<session>` wrapper even if it is not present |
|
||||||
|
| `--dry-run` | Print what would be spawned; spawn and write nothing |
|
||||||
|
| `--herdr-session` (`--herdr-server`) | Isolated herdr server name |
|
||||||
|
| `--herdr-workspace` | herdr workspace label recorded in the registry (a label only, never a socket) |
|
||||||
|
| `--submit-job PROMPT` | Register a delegated job with this prompt and send it to the new agent |
|
||||||
|
| `--onboard` / `--no-onboard` | Submit the onboarding job (default: on) |
|
||||||
|
| `--auto-trust` / `--no-auto-trust` | Answer a start-up directory-trust dialog or leave it to a human (`MAM_AUTO_TRUST`; default: answer) |
|
||||||
|
| `--isolate` / `--no-isolate` | Legacy, no-op |
|
||||||
|
|
||||||
|
| Exit | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| 0 | Created; or `--dry-run` |
|
||||||
|
| 1 | Pre-flight failure, nothing was spawned: workspace not a directory, herdr not installed, agent CLI not in `PATH`, agent not authenticated. After spawn: the task brief could not be written or the instruction injection failed (the session is rolled back) |
|
||||||
|
| 2 | Bad arguments; control-plane binding refused for a worktree; whitespace in a control-plane path that must be forwarded |
|
||||||
|
| 3 | A herdr session of that name already exists (`multi-agent-mux-resume`, or `multi-agent-mux-stop` first) |
|
||||||
|
| 4 | The YAML already has a `running` row of that name. The YAML write raises it after spawn, and the herdr session is rolled back |
|
||||||
|
| 42 | The TUI never became ready and the process is gone |
|
||||||
|
| 43 | The rollback could not kill the herdr session; it is still running |
|
||||||
|
| other | A later step failed and its code is passed through. The EXIT trap (`cleanup_herdr_on_error`) rolls back the herdr session and closes the onboarding job as `cancelled`; if that kill fails, the exit is 43 instead |
|
||||||
|
|
||||||
## Registering the session in agent-sessions.yaml
|
## Registering the session in agent-sessions.yaml
|
||||||
|
|
||||||
After spawn, append a new `herdr_sessions[]` entry to `.mam/agent-sessions.yaml`:
|
`create_session.sh` writes the `herdr_sessions[]` row itself (the format is in **Output format** above); do not append it by hand. `pane.cmd_full` is the actual command line in the pane, decided by the adapter `spawn-spec` (`lib_py/agents`), not the resume command.
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: <SESSION_NAME>
|
|
||||||
status: running
|
|
||||||
herdr_session_created_at: 2026-06-17T...Z # ISO 8601 UTC
|
|
||||||
herdr_session_epoch: <HERDR_EPOCH>
|
|
||||||
herdr_session: <HERDR_SESSION_NAME> # Isolated session name (default: 'mam-<ws-slug>')
|
|
||||||
herdr_server: <HERDR_SESSION_NAME> # Alias for herdr_session
|
|
||||||
pane:
|
|
||||||
index: 0
|
|
||||||
pid: <PANE_PID>
|
|
||||||
cmd: <AGENT> # 'claude' or 'agy'
|
|
||||||
cmd_full: <full command line, see table below>
|
|
||||||
cwd: <PANE_CWD>
|
|
||||||
tui: # only for claude
|
|
||||||
model: <from TUI status>
|
|
||||||
provider: <from TUI status>
|
|
||||||
plan: <from TUI status>
|
|
||||||
account: <from TUI status>
|
|
||||||
version: <from TUI status>
|
|
||||||
start_command: "HERDR_SESSION_NAME=<herdr_session> herdr new-session -d -s <SESSION_NAME> -x 140 -y 40 -c <WORKSPACE> <CMD_FULL>"
|
|
||||||
attach_command: "HERDR_SESSION_NAME=<herdr_session> herdr agent attach <SESSION_NAME>"
|
|
||||||
kill_command: "HERDR_SESSION_NAME=<herdr_session> herdr kill-session -t <SESSION_NAME>"
|
|
||||||
# All three require `source .agents/skills/lib.sh` first — `new-session`/`kill-session`
|
|
||||||
# are tmux-compat pseudo-commands the shim translates, and `HERDR_SESSION_NAME` is what
|
|
||||||
# the shim reads to route to the right isolated herdr *session* (real `herdr` has no
|
|
||||||
# env-var-based scoping of its own; `herdr_session: default` needs no prefix at all).
|
|
||||||
```
|
|
||||||
|
|
||||||
`cmd_full` per agent (this is the actual command line in the pane, not the resume command):
|
|
||||||
|
|
||||||
| agent | cmd_full |
|
|
||||||
|---|---|
|
|
||||||
| claude (interactive) | `claude` |
|
|
||||||
| agy (interactive) | `agy --dangerously-skip-permissions` |
|
|
||||||
|
|
||||||
Use the `agent-sessions-yaml-edit` script in `scripts/` to safely append (preserves comments + format):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \
|
|
||||||
--workspace "$WORKSPACE" --agent "$AGENT" --role "$ROLE" --session "$SESSION_NAME"
|
|
||||||
```
|
|
||||||
|
|
||||||
The script handles the YAML append, pane capture, and the `last_visible_status` placeholder.
|
|
||||||
|
|
||||||
## Issue worktrees: `worktree_prepare.sh` (Issue #21 item 4)
|
## Issue worktrees: `worktree_prepare.sh` (Issue #21 item 4)
|
||||||
|
|
||||||
@@ -321,7 +238,7 @@ print('OK:', names)
|
|||||||
herdr agent read "$SESSION_NAME" --source visible --lines 20 # TUI ready = agent banner visible, no dialog text
|
herdr agent read "$SESSION_NAME" --source visible --lines 20 # TUI ready = agent banner visible, no dialog text
|
||||||
```
|
```
|
||||||
|
|
||||||
> `herdr has-session` / `herdr list-panes` / `herdr capture-pane` above are tmux-compat pseudo-commands only understood after `source .agents/skills/lib.sh` (see `Workflow`) — the real `herdr` binary has no such subcommands. The block above uses the real `herdr agent get`/`herdr agent read` equivalents so it also works standalone.
|
> `herdr has-session` / `herdr list-panes` / `herdr capture-pane` are tmux-compat pseudo-commands only understood after `source .agents/skills/lib.sh` — the real `herdr` binary has no such subcommands. The block above uses only the real `herdr agent get`/`herdr agent read` commands, so it works standalone.
|
||||||
|
|
||||||
## When NOT to use this skill
|
## When NOT to use this skill
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Three cases this skill handles:
|
|||||||
|
|
||||||
1. **herdr is dead, conversation lives** — `agent-sessions.yaml` has the UUID. The JSONL/db is on disk. Re-spawn the herdr session + run `claude -r <id>` / `agy --conversation <id>`.
|
1. **herdr is dead, conversation lives** — `agent-sessions.yaml` has the UUID. The JSONL/db is on disk. Re-spawn the herdr session + run `claude -r <id>` / `agy --conversation <id>`.
|
||||||
2. **herdr is alive but empty** — You started a session with `multi-agent-mux-create` but haven't sent a message yet (so no session id was assigned). The user can either send their first message (and the id is auto-assigned), or you can read the *workspace's* most recent conversation from `$HOME_DIR/.gemini/antigravity-cli/cache/last_conversations.json` (defaults to `~/.gemini/...`) for agy, or the latest `*.jsonl` in `$CLAUDE_PROJECT_DIR/<workspace-key>/` (defaults to `~/.claude/projects/`) for claude.
|
2. **herdr is alive but empty** — You started a session with `multi-agent-mux-create` but haven't sent a message yet (so no session id was assigned). The user can either send their first message (and the id is auto-assigned), or you can read the *workspace's* most recent conversation from `$HOME_DIR/.gemini/antigravity-cli/cache/last_conversations.json` (defaults to `~/.gemini/...`) for agy, or the latest `*.jsonl` in `$CLAUDE_PROJECT_DIR/<workspace-key>/` (defaults to `~/.claude/projects/`) for claude.
|
||||||
3. **herdr is alive AND the agent inside is already running** — Just attach. No re-spawn needed.
|
3. **herdr is alive AND the agent inside is already running** — `resume_session.sh` only updates the row and exits 0. It does not attach and does not check the agent's permission flags (#45); attach with `herdr agent attach <name>`.
|
||||||
|
|
||||||
### Resuming a `stopped` session (`stopped → running`)
|
### Resuming a `stopped` session (`stopped → running`)
|
||||||
|
|
||||||
@@ -57,66 +57,30 @@ If both are empty → for Class A agents (`agy`, `hermes`, `opencode`, `codex`),
|
|||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
WORKSPACE=/path/to/project
|
bash .agents/skills/multi-agent-mux-resume/scripts/resume_session.sh --workspace <path> --agent <agent> --session <name>
|
||||||
AGENT=claude # claude | agy | hermes | grok | opencode | codex — pass it explicitly
|
|
||||||
SESSION_NAME=<workspace>-creator-<agent> # same convention as multi-agent-mux-create
|
|
||||||
|
|
||||||
# Resolve the isolated herdr server name & load common utils
|
|
||||||
source .agents/skills/lib.sh
|
|
||||||
|
|
||||||
# 1. Resolve the session id (pass session name to prefer target-row recorded id)
|
|
||||||
UUID=$(bash .agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.sh \
|
|
||||||
--workspace "$WORKSPACE" --agent "$AGENT" --session "$SESSION_NAME")
|
|
||||||
|
|
||||||
if [ -z "$UUID" ]; then
|
|
||||||
case "$AGENT" in
|
|
||||||
agy|hermes|opencode|codex)
|
|
||||||
: # resume_session.sh handles fresh-spawn fallback automatically (FRESH_SPAWN=1)
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "ERROR: No saved session for $WORKSPACE ($AGENT). Use multi-agent-mux-create first." >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
export HERDR_SESSION_NAME="$(resolve_herdr_session "$SESSION_NAME" "$WORKSPACE")"
|
|
||||||
export MAM_WS_LABEL="${HERDR_WORKSPACE_OPT:-$(resolve_herdr_workspace "$SESSION_NAME" "$WORKSPACE")}"
|
|
||||||
|
|
||||||
# 2. If herdr is alive, attach. Done.
|
|
||||||
if herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
|
|
||||||
echo "herdr '$SESSION_NAME' already running. Attaching..."
|
|
||||||
exec herdr agent attach "$SESSION_NAME"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. Determine CMD_FULL
|
|
||||||
# For claude: if assigned UUID transcript .jsonl is unmaterialized on disk, use --session-id <uuid>; otherwise use -r <uuid>
|
|
||||||
case "$AGENT" in
|
|
||||||
claude) CMD_FULL="claude --dangerously-skip-permissions $CLAUDE_ID_FLAG $UUID" ;;
|
|
||||||
agy) CMD_FULL="agy --dangerously-skip-permissions --conversation $UUID" ;;
|
|
||||||
hermes) CMD_FULL="hermes --resume $UUID --no-restore-cwd --yolo --accept-hooks" ;;
|
|
||||||
grok) CMD_FULL="grok --resume $UUID --permission-mode bypassPermissions" ;;
|
|
||||||
opencode) CMD_FULL="opencode --session $UUID --auto --agent build" ;;
|
|
||||||
codex) CMD_FULL="codex resume --dangerously-bypass-approvals-and-sandbox $UUID" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# 4. Spawn new herdr session + run agent with the saved id
|
|
||||||
eval "herdr new-session -d -s \"$SESSION_NAME\" -x 140 -y 40 -c \"$WORKSPACE\" \"$CMD_FULL\""
|
|
||||||
# Issue #24: start-up dialogs (trust prompts, blocking notices) are answered for
|
|
||||||
# every agent from the adapter's signatures - the same call create makes.
|
|
||||||
# --no-auto-trust leaves a directory-trust dialog to a human (MAM_AUTO_TRUST=0).
|
|
||||||
handle_startup_dialogs "$SESSION_NAME" 20 "$AGENT"
|
|
||||||
|
|
||||||
# 4. Update agent-sessions.yaml: status running, last_visible_status
|
|
||||||
# (Also automatically publishes a `progress --detail "resumed"` event to the multi-agent-mux-delegate-job registry if a delegate_job_id exists)
|
|
||||||
bash .agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh \
|
|
||||||
--session "$SESSION_NAME" --uuid "$UUID"
|
|
||||||
|
|
||||||
# 5. Attach (real native command — "attach" isn't in the lib.sh tmux-compat shim,
|
|
||||||
# and real herdr has no `-t` flag here, only a positional target)
|
|
||||||
herdr agent attach "$SESSION_NAME"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`resume_session.sh` does the whole flow: original-cwd check, control-plane binding, session-id resolution (`resolve_session_id.sh`), the herdr-alive check, the agent command (adapter `resume-spec`, or `spawn-spec` for a Class A fresh spawn), spawn, start-up dialogs, and the YAML update (`update_yaml_resumed.sh`, which also publishes a `progress "resumed"` event when the row has a `delegate_job_id`). It never attaches; attaching is a separate step: `herdr agent attach <name>`.
|
||||||
|
|
||||||
|
| Option | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `--workspace` | Required. The directory the session ran in (its row's `pane.cwd`; a different path is exit 2) |
|
||||||
|
| `--agent` | Required. `claude`, `agy`, `hermes`, `grok`, `opencode`, `codex` |
|
||||||
|
| `--session` | Required. Session name |
|
||||||
|
| `--herdr-session` (`--herdr-server`) | Isolated herdr server name |
|
||||||
|
| `--herdr-workspace` | herdr workspace label |
|
||||||
|
| `--auto-trust` / `--no-auto-trust` | Answer a start-up directory-trust dialog or leave it to a human (`MAM_AUTO_TRUST`; default: answer) |
|
||||||
|
| `--dry-run` | Resolve the command and print it; write nothing |
|
||||||
|
|
||||||
|
| Exit | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| 0 | Resumed; or already running, so only the row was updated (no attach); or `--dry-run` |
|
||||||
|
| 1 | From the checks before spawn, nothing was changed: no saved session for claude/grok (use `multi-agent-mux-create` first), agent binary missing or not executable. **The YAML update after spawn (`update_yaml_resumed.sh`) also exits 1** (YAML not found) |
|
||||||
|
| 2 | From the checks before spawn, nothing was changed: bad arguments, `--workspace` differs from the original cwd or it no longer exists, control-plane binding refused, whitespace in the control-plane path. **The YAML update after spawn also exits 2** (bad arguments) |
|
||||||
|
| other | A later step failed (herdr spawn, start-up dialogs, YAML update) and its code is passed through. resume does not roll back |
|
||||||
|
|
||||||
|
A non-zero exit after the spawn can leave the herdr session running while the row is still `stopped`. Before retrying or creating a new session, check with `multi-agent-mux-status`. The `ERROR:` line on stderr tells the two apart: the pre-spawn checks' messages are in `resume_session.sh` (lines 50-223).
|
||||||
|
|
||||||
## Original cwd only (Issue #21 phase 2)
|
## Original cwd only (Issue #21 phase 2)
|
||||||
|
|
||||||
A session is resumed only in the directory it ran in (its row's `pane.cwd`): the conversation is keyed by that directory, and a class A agent (agy/hermes/opencode/codex) would otherwise start a new conversation in the wrong tree.
|
A session is resumed only in the directory it ran in (its row's `pane.cwd`): the conversation is keyed by that directory, and a class A agent (agy/hermes/opencode/codex) would otherwise start a new conversation in the wrong tree.
|
||||||
@@ -130,7 +94,7 @@ A session is resumed only in the directory it ran in (its row's `pane.cwd`): the
|
|||||||
|
|
||||||
- **`claude -r` requires the SAME project directory** — if the workspace path differs from when the session was created, claude will create a new project dir key (`-home-...-different-name`) and put the resume in a different location. Always `-c` (cd to workspace) before running.
|
- **`claude -r` requires the SAME project directory** — if the workspace path differs from when the session was created, claude will create a new project dir key (`-home-...-different-name`) and put the resume in a different location. Always `-c` (cd to workspace) before running.
|
||||||
- **agy's `--conversation` flag name varies by version** — older versions used `--resume` or `-r`. Check `agy --help | grep -E "conversation|resume"` and use the right flag. v1.0.x: `--conversation`.
|
- **agy's `--conversation` flag name varies by version** — older versions used `--resume` or `-r`. Check `agy --help | grep -E "conversation|resume"` and use the right flag. v1.0.x: `--conversation`.
|
||||||
- **The first message after resume might re-trigger TUI dialogs** — if the original session was created with `--dangerously-skip-permissions`, those flags are NOT persisted; you must re-apply them on resume. The script above re-passes them.
|
- **The first message after resume might re-trigger TUI dialogs** — if the original session was created with `--dangerously-skip-permissions`, those flags are NOT persisted; you must re-apply them on resume. `resume_session.sh` re-passes them through the adapter `resume-spec`.
|
||||||
- **Don't resume if the session is brand new and empty** — `multi-agent-mux-create` already set up an empty container; sending a probe message ("init") is the right way to materialize a session id, NOT `claude -r` with a placeholder.
|
- **Don't resume if the session is brand new and empty** — `multi-agent-mux-create` already set up an empty container; sending a probe message ("init") is the right way to materialize a session id, NOT `claude -r` with a placeholder.
|
||||||
- **`agy --conversation <id>` will fail if the conversation was deleted from disk** — check `~/.gemini/antigravity-cli/conversations/<uuid>.db` exists before attempting resume. If missing, the conversation is gone; you need a fresh session via `multi-agent-mux-create`.
|
- **`agy --conversation <id>` will fail if the conversation was deleted from disk** — check `~/.gemini/antigravity-cli/conversations/<uuid>.db` exists before attempting resume. If missing, the conversation is gone; you need a fresh session via `multi-agent-mux-create`.
|
||||||
|
|
||||||
@@ -159,10 +123,10 @@ herdr agent read "$SESSION_NAME" --source visible --lines 30
|
|||||||
# look for the previous message at top of the buffer (claude) or last_visible_status set (agy)
|
# look for the previous message at top of the buffer (claude) or last_visible_status set (agy)
|
||||||
```
|
```
|
||||||
|
|
||||||
> `herdr list-panes` / `herdr capture-pane` here are tmux-compat pseudo-commands that only
|
> `herdr list-panes` / `herdr capture-pane` are tmux-compat pseudo-commands that only
|
||||||
> work after `source .agents/skills/lib.sh` (as done in `Workflow` above) — the real `herdr`
|
> work after `source .agents/skills/lib.sh` — the real `herdr` binary doesn't have those
|
||||||
> binary doesn't have those subcommands. This block uses the real `herdr agent get`/`herdr agent read`
|
> subcommands. This block uses only the real `herdr agent get`/`herdr agent read` commands,
|
||||||
> equivalents instead so it also works standalone.
|
> so it works standalone.
|
||||||
|
|
||||||
## When NOT to use this skill
|
## When NOT to use this skill
|
||||||
|
|
||||||
|
|||||||
@@ -73,31 +73,40 @@ lab-paper-pdf2md-creator-claude default running alive clau
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"yaml_path": "...",
|
"timestamp": "2026-06-19T14:20:00Z",
|
||||||
"herdr_sessions_alive": ["..."],
|
"yaml_path": ".../.mam/agent-sessions.yaml",
|
||||||
"yaml_entries": [...],
|
"herdr_sessions_alive": ["lab-landing-page-creator-claude|default"],
|
||||||
"rows": [
|
"herdr_confirmed": true,
|
||||||
|
"drifts": [
|
||||||
|
{"class": "A", "name": "lab-landing-page-creator-agy", "msg": "..."}
|
||||||
|
],
|
||||||
|
"actions": [],
|
||||||
|
"sessions_detail": [
|
||||||
{
|
{
|
||||||
"name": "lab-landing-page-creator-claude",
|
"name": "lab-landing-page-creator-claude",
|
||||||
"yaml_status": "running",
|
"server": "default",
|
||||||
|
"herdr_workspace": "landing-page",
|
||||||
|
"status": "running",
|
||||||
"herdr_alive": true,
|
"herdr_alive": true,
|
||||||
"pane_cmd": "claude",
|
"cmd": "claude",
|
||||||
|
"role": "creator",
|
||||||
|
"resume_state": "yes",
|
||||||
|
"job_id": null,
|
||||||
|
"job_status": null,
|
||||||
"pane_cwd": "/home/.../refer_landing_page",
|
"pane_cwd": "/home/.../refer_landing_page",
|
||||||
"resume_uuid_on_disk": true,
|
"attach_command": "HERDR_SESSION_NAME=default herdr agent attach lab-landing-page-creator-claude",
|
||||||
"drift": null
|
"drift_classes": [],
|
||||||
},
|
"pane_pid": 12345,
|
||||||
{
|
"cmd_full": "claude --dangerously-skip-permissions --session-id <uuid>",
|
||||||
"name": "lab-landing-page-creator-agy",
|
"start_command": "...",
|
||||||
"yaml_status": "terminated",
|
"last_visible_status": "pinned"
|
||||||
"herdr_alive": false,
|
|
||||||
"drift": "yaml-says-terminated-but-disk-uuid-still-present"
|
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"unregistered": [],
|
|
||||||
"drifts": []
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The top-level keys other than `sessions_detail` are `reconcile.sh --once --emit-diff --dry-run`'s output, passed through unchanged. `resume_state` is `yes`, `MISSING`, `scan` or `no`.
|
||||||
|
|
||||||
## Drift classes (read-only — never mutates)
|
## Drift classes (read-only — never mutates)
|
||||||
|
|
||||||
| Class | Detection | Meaning |
|
| Class | Detection | Meaning |
|
||||||
|
|||||||
@@ -35,32 +35,7 @@ The stop command is always **graceful by default**:
|
|||||||
|
|
||||||
## Pre-flight
|
## Pre-flight
|
||||||
|
|
||||||
```bash
|
`stop_session.sh` checks this itself: the YAML exists and the session is registered (otherwise exit 1), and a session already `stopped` is a no-op (exit 0).
|
||||||
SESSION_NAME=<workspace>-creator-<agent> # convention
|
|
||||||
AGENT=claude # claude | agy | hermes | grok | opencode | codex — always pass it
|
|
||||||
AGENT_SESSIONS_YAML=.mam/agent-sessions.yaml
|
|
||||||
|
|
||||||
# 1) Session is registered?
|
|
||||||
python3 -c "
|
|
||||||
import yaml
|
|
||||||
d = yaml.safe_load(open('$AGENT_SESSIONS_YAML'))
|
|
||||||
names = [s['name'] for s in d.get('herdr_sessions', [])]
|
|
||||||
if '$SESSION_NAME' not in names:
|
|
||||||
print('NOT in YAML — refusing to stop (no audit trail). Use multi-agent-mux-create first, or pass --force-no-yaml.')
|
|
||||||
raise SystemExit(1)
|
|
||||||
"
|
|
||||||
|
|
||||||
# 2) Already stopped?
|
|
||||||
ALREADY=$(python3 -c "
|
|
||||||
import yaml
|
|
||||||
d = yaml.safe_load(open('$AGENT_SESSIONS_YAML'))
|
|
||||||
s = [x for x in d['herdr_sessions'] if x['name']=='$SESSION_NAME'][0]
|
|
||||||
print(s.get('status', 'unknown'))
|
|
||||||
")
|
|
||||||
if [ "$ALREADY" = "stopped" ]; then
|
|
||||||
echo "Already stopped."
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
@@ -76,9 +51,22 @@ bash .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \
|
|||||||
# 3. Stop gracefully + clean up on-disk conversation (DANGEROUS)
|
# 3. Stop gracefully + clean up on-disk conversation (DANGEROUS)
|
||||||
# — this prevents any future resume (status=terminated, resumable=false).
|
# — this prevents any future resume (status=terminated, resumable=false).
|
||||||
bash .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \
|
bash .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \
|
||||||
--session "$SESSION_NAME" --agent "$AGENT" --purge-conversation
|
--session "$SESSION_NAME" --agent "$AGENT" --purge-conversation --yes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`--purge-conversation` cannot be undone. Pass `--yes` only after the user has confirmed it.
|
||||||
|
|
||||||
|
| Exit | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| 0 | Stopped; or already `stopped` (no-op) |
|
||||||
|
| 1 | YAML not found, or the session is not registered |
|
||||||
|
| 2 | Bad arguments (including the retired `--mode` / `--capture-id` / `--graceful`), or `--agent` omitted and not resolvable |
|
||||||
|
| 3 | `--purge-conversation` without `--yes` |
|
||||||
|
| 4 | Purge aborted: the herdr session survived the kill chain |
|
||||||
|
| other | The registry update after the stop (YAML / DB write) failed and its code is passed through |
|
||||||
|
|
||||||
|
Exits 1, 2 and 3 come from checks before anything is stopped, so nothing was changed. On an `other` exit the session may already be down while its row still says `running`: check with `multi-agent-mux-status`, then run the stop again (it is idempotent).
|
||||||
|
|
||||||
**Idempotency**: if the row is already `status: stopped`, the script prints `already stopped (...)` and exits 0 — re-running is a safe no-op.
|
**Idempotency**: if the row is already `status: stopped`, the script prints `already stopped (...)` and exits 0 — re-running is a safe no-op.
|
||||||
|
|
||||||
**`--agent` is the standard.** Pass it on every invocation. If omitted, the script
|
**`--agent` is the standard.** Pass it on every invocation. If omitted, the script
|
||||||
|
|||||||
@@ -0,0 +1,621 @@
|
|||||||
|
{
|
||||||
|
"base": "45d1621",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-create",
|
||||||
|
"start": 63,
|
||||||
|
"end": 66,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "e44d91b9be46",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "example",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh:99",
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh:356"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "HERDR_SESSION_NAME environment example"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-create",
|
||||||
|
"start": 68,
|
||||||
|
"end": 70,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "dd27e3eccbbe",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh:82-83"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "--herdr-session / --herdr-workspace example"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-create",
|
||||||
|
"start": 73,
|
||||||
|
"end": 75,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "7c8f836dd518",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh:84"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "--submit-job example"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-create",
|
||||||
|
"start": 78,
|
||||||
|
"end": 80,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "8a5226fbc7a0",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh:85"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "--onboard example"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-create",
|
||||||
|
"start": 127,
|
||||||
|
"end": 145,
|
||||||
|
"lang": "yaml",
|
||||||
|
"sha": "2dd608d04ae0",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "example",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh:518-545"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "Output format: the row create_session.sh writes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-create",
|
||||||
|
"start": 151,
|
||||||
|
"end": 153,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "613003708f19",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh:29-52"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "Workflow: the entry point (replaces the removed Workflow block, base 168-220)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-create",
|
||||||
|
"start": 191,
|
||||||
|
"end": 195,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "38836d31bf38",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-create/scripts/worktree_prepare.sh:24",
|
||||||
|
"multi-agent-mux-create/scripts/worktree_prepare.sh:250"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "worktree_prepare.sh call"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-create",
|
||||||
|
"start": 217,
|
||||||
|
"end": 239,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "d90cfd2555fd",
|
||||||
|
"class": "b",
|
||||||
|
"kind": "verification",
|
||||||
|
"evidence": [],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "post-create checks (herdr alive, cmd/cwd, YAML row, TUI); no script offers them as a command - stage 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-delegate-job",
|
||||||
|
"start": 36,
|
||||||
|
"end": 81,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "9fcfb4503a40",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-delegate-job/multi-agent-mux-delegate-job:1236-1247"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "CLI reference; subcommands submit/status/list/verify/wait/logs match main()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-loop",
|
||||||
|
"start": 50,
|
||||||
|
"end": 53,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "cbc86a648420",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-loop/scripts/run_loop.sh:45-46"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "environment + run_loop.sh call"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-loop",
|
||||||
|
"start": 70,
|
||||||
|
"end": 93,
|
||||||
|
"lang": "-",
|
||||||
|
"sha": "7f11fedca5f9",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "diagram",
|
||||||
|
"evidence": [],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "role diagram"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-loop",
|
||||||
|
"start": 121,
|
||||||
|
"end": 168,
|
||||||
|
"lang": "mermaid",
|
||||||
|
"sha": "093c1673b128",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "diagram",
|
||||||
|
"evidence": [],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "sequence diagram (mermaid)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-loop",
|
||||||
|
"start": 228,
|
||||||
|
"end": 234,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "1b75c515c99a",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"lib_py/loop/runner.py:163",
|
||||||
|
"multi-agent-mux-create/scripts/worktree_prepare.sh:250"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "--worktree/--base --review-only call pattern"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-loop",
|
||||||
|
"start": 240,
|
||||||
|
"end": 265,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "9b061da667f8",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"lib_py/loop/runner.py:1787"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "three call examples; every flag is parsed by runner.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-monitor",
|
||||||
|
"start": 60,
|
||||||
|
"end": 66,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "d6a74c6482a9",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-monitor/scripts/reconcile.sh:48"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "reconcile.sh --subscribe call"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-monitor",
|
||||||
|
"start": 109,
|
||||||
|
"end": 115,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "55a7f7ec1a57",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-monitor/scripts/reconcile.sh:45-47"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "--once --emit-diff [--dry-run] call"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-monitor",
|
||||||
|
"start": 128,
|
||||||
|
"end": 133,
|
||||||
|
"lang": "-",
|
||||||
|
"sha": "e80f013a7fc2",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "example",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-monitor/scripts/reconcile.sh:601"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "drift A example"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-monitor",
|
||||||
|
"start": 144,
|
||||||
|
"end": 150,
|
||||||
|
"lang": "-",
|
||||||
|
"sha": "4924c8c79db5",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "example",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-monitor/scripts/reconcile.sh:708"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "drift B example"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-monitor",
|
||||||
|
"start": 156,
|
||||||
|
"end": 162,
|
||||||
|
"lang": "-",
|
||||||
|
"sha": "3f0b027ff51e",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "example",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-monitor/scripts/reconcile.sh:735"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "drift C example (indented fence)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-monitor",
|
||||||
|
"start": 182,
|
||||||
|
"end": 210,
|
||||||
|
"lang": "markdown",
|
||||||
|
"sha": "3f5d82c08fd9",
|
||||||
|
"class": "b",
|
||||||
|
"kind": "procedure",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-monitor/scripts/reconcile.sh:41"
|
||||||
|
],
|
||||||
|
"mismatch": [
|
||||||
|
"M1: the spec says `sleep 30`; reconcile.sh polls every RECONCILE_POLL_INTERVAL, default 15"
|
||||||
|
],
|
||||||
|
"note": "agent-run polling loop; \"report only new drifts\" and the Hard rules are agent-side guidance - stage 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-monitor",
|
||||||
|
"start": 229,
|
||||||
|
"end": 233,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "b3588563316e",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-monitor/scripts/reconcile.sh:45-46"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "one-shot run piped to json.tool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-orc-onboard",
|
||||||
|
"start": 22,
|
||||||
|
"end": 24,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "6e26222260bb",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-orc-onboard/scripts/orc_onboard.sh:20-22"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "entry point call"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-orc-onboard",
|
||||||
|
"start": 40,
|
||||||
|
"end": 46,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "1be6e95c82c7",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-orc-onboard/scripts/orc_onboard.sh:48-62"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "--remove --purge-conversation --yes call + output"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-resume",
|
||||||
|
"start": 59,
|
||||||
|
"end": 61,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "5fe43f9fb460",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-resume/scripts/resume_session.sh:10-21"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "Workflow: the entry point (replaces the removed Workflow block, base 59-118)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-resume",
|
||||||
|
"start": 103,
|
||||||
|
"end": 124,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "f4bddf5ba4d1",
|
||||||
|
"class": "b",
|
||||||
|
"kind": "verification",
|
||||||
|
"evidence": [],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "post-resume checks (herdr agent get, YAML, screen); no script offers them as a command - stage 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-status",
|
||||||
|
"start": 30,
|
||||||
|
"end": 34,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "ed6251410f1f",
|
||||||
|
"class": "b",
|
||||||
|
"kind": "procedure",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-status/scripts/status.sh:22"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "pre-flight: the YAML check duplicates status.sh:22, but status.sh has no has_real_herdr check (it reports dead without herdr) - mixed, kept"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-status",
|
||||||
|
"start": 44,
|
||||||
|
"end": 46,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "27cab128411c",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-status/scripts/status.sh:20"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "entry point call"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-status",
|
||||||
|
"start": 61,
|
||||||
|
"end": 70,
|
||||||
|
"lang": "-",
|
||||||
|
"sha": "7e5ec388a64a",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "example",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-status/scripts/status.sh:411"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "table output example"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-status",
|
||||||
|
"start": 74,
|
||||||
|
"end": 106,
|
||||||
|
"lang": "json",
|
||||||
|
"sha": "cfea3a10dc8d",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "example",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-status/scripts/status.sh:31-36",
|
||||||
|
"multi-agent-mux-status/scripts/status.sh:209-232",
|
||||||
|
"multi-agent-mux-monitor/scripts/reconcile.sh:1084"
|
||||||
|
],
|
||||||
|
"mismatch": [
|
||||||
|
"S1 (fixed): the example used rows/yaml_entries/unregistered; the real keys are reconcile.sh's drift object plus sessions_detail"
|
||||||
|
],
|
||||||
|
"note": "--json output example"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-stop",
|
||||||
|
"start": 42,
|
||||||
|
"end": 55,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "27ebf150b92a",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-stop/scripts/stop_session.sh:1-33"
|
||||||
|
],
|
||||||
|
"mismatch": [
|
||||||
|
"T2 (fixed): the --purge-conversation example lacked --yes and would exit 3"
|
||||||
|
],
|
||||||
|
"note": "Workflow: three entry point calls"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-stop",
|
||||||
|
"start": 81,
|
||||||
|
"end": 85,
|
||||||
|
"lang": "-",
|
||||||
|
"sha": "a2b56ac238ae",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "diagram",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-stop/scripts/stop_session.sh:184-198"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "state machine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-stop",
|
||||||
|
"start": 102,
|
||||||
|
"end": 104,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "4189d73d6896",
|
||||||
|
"class": "c",
|
||||||
|
"kind": "invocation",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-stop/scripts/worktree_remove.sh:35-36"
|
||||||
|
],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "worktree_remove.sh call"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-stop",
|
||||||
|
"start": 127,
|
||||||
|
"end": 145,
|
||||||
|
"lang": "bash",
|
||||||
|
"sha": "bcaa2a03484e",
|
||||||
|
"class": "b",
|
||||||
|
"kind": "verification",
|
||||||
|
"evidence": [],
|
||||||
|
"mismatch": null,
|
||||||
|
"note": "post-stop checks; no script offers them as a command - stage 3"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"removed": [
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-resume",
|
||||||
|
"base_start": 59,
|
||||||
|
"base_end": 118,
|
||||||
|
"lang": "bash",
|
||||||
|
"base_sha": "63abe1bb3542",
|
||||||
|
"class": "a",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-resume/scripts/resume_session.sh:111-130",
|
||||||
|
"multi-agent-mux-resume/scripts/resume_session.sh:133-144",
|
||||||
|
"multi-agent-mux-resume/scripts/resume_session.sh:147-158",
|
||||||
|
"multi-agent-mux-resume/scripts/resume_session.sh:172-197",
|
||||||
|
"multi-agent-mux-resume/scripts/resume_session.sh:237",
|
||||||
|
"multi-agent-mux-resume/scripts/resume_session.sh:241",
|
||||||
|
"multi-agent-mux-resume/scripts/resume_session.sh:247-250",
|
||||||
|
"lib_py/agents/adapters/claude.py:109"
|
||||||
|
],
|
||||||
|
"mismatch": [
|
||||||
|
"R1: an already-running session is exec-attached in the doc; the script only updates the row and exits 0 (no attach, no permission-flag check, #45)",
|
||||||
|
"R2: the script never attaches; attach is a separate human step",
|
||||||
|
"R3: steps missing from the doc: original-cwd check, control-plane binding, binary validation, whitespace refusal, --dry-run",
|
||||||
|
"R4: $CLAUDE_ID_FLAG is undefined; the script uses adapter resume-spec (table fallback); no -x 140 -y 40 on resume",
|
||||||
|
"R5: the script passes --agent --workspace --herdr-session --cmd-full [--herdr-workspace] to update_yaml_resumed.sh"
|
||||||
|
],
|
||||||
|
"replacement": "bash .agents/skills/multi-agent-mux-resume/scripts/resume_session.sh --workspace <path> --agent <agent> --session <name>",
|
||||||
|
"script": "multi-agent-mux-resume/scripts/resume_session.sh",
|
||||||
|
"note": "Workflow re-wrote resume_session.sh in bash"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-stop",
|
||||||
|
"base_start": 38,
|
||||||
|
"base_end": 63,
|
||||||
|
"lang": "bash",
|
||||||
|
"base_sha": "a7ed3e902cae",
|
||||||
|
"class": "a",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-stop/scripts/stop_session.sh:99",
|
||||||
|
"multi-agent-mux-stop/scripts/stop_session.sh:140-143",
|
||||||
|
"multi-agent-mux-stop/scripts/stop_session.sh:184-188"
|
||||||
|
],
|
||||||
|
"mismatch": [
|
||||||
|
"T1: the doc offered --force-no-yaml, which no script accepts"
|
||||||
|
],
|
||||||
|
"replacement": "bash .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh",
|
||||||
|
"script": "multi-agent-mux-stop/scripts/stop_session.sh",
|
||||||
|
"note": "pre-flight (registered? already stopped?); replaced by one prose line, the Workflow block already calls the entry point"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-create",
|
||||||
|
"base_start": 35,
|
||||||
|
"base_end": 52,
|
||||||
|
"lang": "bash",
|
||||||
|
"base_sha": "4d9c56a7bd7e",
|
||||||
|
"class": "a",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh:111",
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh:114",
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh:115",
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh:117-156"
|
||||||
|
],
|
||||||
|
"mismatch": [
|
||||||
|
"the doc checks both claude and agy; the script checks only the selected agent (the script is right)"
|
||||||
|
],
|
||||||
|
"replacement": "bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh --workspace <path> --agent <agent> --role <role>",
|
||||||
|
"script": "multi-agent-mux-create/scripts/create_session.sh",
|
||||||
|
"note": "pre-flight; replaced by prose, the has_real_herdr vs command -v pitfall kept as prose"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-create",
|
||||||
|
"base_start": 168,
|
||||||
|
"base_end": 220,
|
||||||
|
"lang": "bash",
|
||||||
|
"base_sha": "00502b0bc2f4",
|
||||||
|
"class": "a",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh:166-168",
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh:207",
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh:259-274",
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh:361-398",
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh:410-412"
|
||||||
|
],
|
||||||
|
"mismatch": [
|
||||||
|
"C1: an existing session is exit 1 in the doc, exit 3 in the script",
|
||||||
|
"C2: the doc sleeps 6 s; the script waits with wait_for_tui_ready and exits 42/43",
|
||||||
|
"C3: the doc hard-codes per-agent commands and starts claude without flags; the script uses adapter spawn-spec"
|
||||||
|
],
|
||||||
|
"replacement": "bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh --workspace <path> --agent <agent> --role <role>",
|
||||||
|
"script": "multi-agent-mux-create/scripts/create_session.sh",
|
||||||
|
"note": "Workflow re-wrote create_session.sh in bash"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"skill": "multi-agent-mux-create",
|
||||||
|
"base_start": 226,
|
||||||
|
"base_end": 252,
|
||||||
|
"lang": "yaml",
|
||||||
|
"base_sha": "f71be0f26fb8",
|
||||||
|
"class": "a",
|
||||||
|
"evidence": [
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh:491-610"
|
||||||
|
],
|
||||||
|
"mismatch": [],
|
||||||
|
"replacement": "bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh --workspace <path> --agent <agent> --role <role>",
|
||||||
|
"script": "multi-agent-mux-create/scripts/create_session.sh",
|
||||||
|
"note": "manual \"append this row\" step; the script writes the row. The lib.sh shim note (base 248-251) moved to prose under Output format"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"entry_points": {
|
||||||
|
"multi-agent-mux-resume/scripts/resume_session.sh": {
|
||||||
|
"skill": "multi-agent-mux-resume",
|
||||||
|
"exit_codes": [
|
||||||
|
"0",
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"other"
|
||||||
|
],
|
||||||
|
"passthrough": {}
|
||||||
|
},
|
||||||
|
"multi-agent-mux-stop/scripts/stop_session.sh": {
|
||||||
|
"skill": "multi-agent-mux-stop",
|
||||||
|
"exit_codes": [
|
||||||
|
"0",
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"other"
|
||||||
|
],
|
||||||
|
"passthrough": {}
|
||||||
|
},
|
||||||
|
"multi-agent-mux-create/scripts/create_session.sh": {
|
||||||
|
"skill": "multi-agent-mux-create",
|
||||||
|
"exit_codes": [
|
||||||
|
"0",
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"42",
|
||||||
|
"43",
|
||||||
|
"other"
|
||||||
|
],
|
||||||
|
"passthrough": {
|
||||||
|
"4": "multi-agent-mux-create/scripts/create_session.sh:515"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prose_mismatches": [
|
||||||
|
"C4: create named a nonexistent `agent-sessions-yaml-edit` script; removed (create_session.sh writes the row)",
|
||||||
|
"C5: create's cmd_full table (claude = `claude`) disagreed with adapter spawn-spec; replaced by \"decided by spawn-spec\"",
|
||||||
|
"C6: create_session.sh header lists exit 4 \"YAML append failure\"; there is no literal `exit 4`, but the YAML write raises SystemExit(4) for a duplicate running row (create_session.sh:515) and it passes through, so 4 IS a real exit code (the plan assumed it was not)",
|
||||||
|
"R6: resume's \"The script above re-passes them\" and \"as done in Workflow above\" pointed at the removed block; reworded (same for create's Verification note)"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
"""Issue #42 stages 1-2: the SKILL.md code-block inventory is data, and the docs follow it.
|
||||||
|
|
||||||
|
tests/data/skill_code_blocks.json lists every fenced block of the eight
|
||||||
|
SKILL.md files with its class - (b) a procedure no script offers yet, (c) an
|
||||||
|
example, call or diagram - and the (a) blocks removed because the entry script
|
||||||
|
already does them. These tests fail when a SKILL.md block changes without the
|
||||||
|
inventory, when an (a) block comes back, or when an exit-code table drifts from
|
||||||
|
the entry script.
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
SKILLS = REPO / ".agents" / "skills"
|
||||||
|
DATA = json.loads((Path(__file__).parent / "data" / "skill_code_blocks.json").read_text(encoding="utf-8"))
|
||||||
|
SKILL_NAMES = sorted(p.parent.name for p in SKILLS.glob("multi-agent-mux-*/SKILL.md"))
|
||||||
|
KINDS = {"invocation", "example", "diagram", "procedure", "verification"}
|
||||||
|
|
||||||
|
_OPEN = re.compile(r"^(\s*)(`{3,}|~{3,})\s*([^`\s]*)")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_blocks(text):
|
||||||
|
"""Fenced blocks as (start, end, lang, sha, body); 1-based fence lines.
|
||||||
|
|
||||||
|
Indented fences (inside list items) count: they render as code blocks too.
|
||||||
|
A fence closes on a line of the same character, at least as long."""
|
||||||
|
lines = text.split("\n")
|
||||||
|
out, i = [], 0
|
||||||
|
while i < len(lines):
|
||||||
|
m = _OPEN.match(lines[i])
|
||||||
|
if not m:
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
fence = m.group(2)
|
||||||
|
close = re.compile(r"^\s*" + re.escape(fence[0]) + "{%d,}\\s*$" % len(fence))
|
||||||
|
j = i + 1
|
||||||
|
while j < len(lines) and not close.match(lines[j]):
|
||||||
|
j += 1
|
||||||
|
body = "\n".join(lines[i + 1:j])
|
||||||
|
out.append((i + 1, j + 1, m.group(3) or "-", hashlib.sha256(body.encode()).hexdigest()[:12], body))
|
||||||
|
i = j + 1
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def skill_text(skill):
|
||||||
|
return (SKILLS / skill / "SKILL.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
_HEREDOC = re.compile(r"(?<!<)<<-?\s*(['\"]?)(\w+)\1")
|
||||||
|
_EXIT = re.compile(r"(?<![\w.])exit\s+(\d+)\b")
|
||||||
|
|
||||||
|
|
||||||
|
def literal_exit_codes(script):
|
||||||
|
"""Codes the entry script exits with literally: `exit N` in shell context.
|
||||||
|
|
||||||
|
Only the entry file itself (not lib.sh or sub-scripts); heredoc bodies and
|
||||||
|
comment lines are skipped, and `sys.exit(N)` never matches."""
|
||||||
|
codes, pending = set(), []
|
||||||
|
for line in (SKILLS / script).read_text(encoding="utf-8").split("\n"):
|
||||||
|
if pending:
|
||||||
|
if line.strip() == pending[0]:
|
||||||
|
pending.pop(0)
|
||||||
|
continue
|
||||||
|
if re.match(r"^\s*#", line):
|
||||||
|
continue
|
||||||
|
codes.update(_EXIT.findall(line))
|
||||||
|
pending.extend(m.group(2) for m in _HEREDOC.finditer(line))
|
||||||
|
return codes
|
||||||
|
|
||||||
|
|
||||||
|
def exit_table(skill):
|
||||||
|
"""First-column values of the SKILL.md `| Exit | Meaning |` table, in order."""
|
||||||
|
lines = skill_text(skill).split("\n")
|
||||||
|
heads = [i for i, l in enumerate(lines) if re.match(r"^\|\s*Exit\s*\|\s*Meaning\s*\|", l)]
|
||||||
|
assert len(heads) == 1, f"{skill}: expected one '| Exit | Meaning |' table, found {len(heads)}"
|
||||||
|
rows = []
|
||||||
|
for l in lines[heads[0] + 2:]:
|
||||||
|
if not l.startswith("|"):
|
||||||
|
break
|
||||||
|
rows.append(l.split("|")[1].strip())
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _evidence_line(ref):
|
||||||
|
path, rng = ref.rsplit(":", 1)
|
||||||
|
first, _, last = rng.partition("-")
|
||||||
|
return SKILLS / path, int(first), int(last or first)
|
||||||
|
|
||||||
|
|
||||||
|
# 1. The inventory and the docs are the same.
|
||||||
|
@pytest.mark.parametrize("skill", SKILL_NAMES)
|
||||||
|
def test_inventory_matches_skill_md(skill):
|
||||||
|
actual = [b[:4] for b in parse_blocks(skill_text(skill))]
|
||||||
|
listed = [(b["start"], b["end"], b["lang"], b["sha"]) for b in DATA["blocks"] if b["skill"] == skill]
|
||||||
|
assert listed == actual, (
|
||||||
|
f"{skill}/SKILL.md blocks differ from tests/data/skill_code_blocks.json.\n"
|
||||||
|
f"actual (start, end, lang, sha): {json.dumps(actual)}")
|
||||||
|
|
||||||
|
|
||||||
|
def test_inventory_covers_eight_skills():
|
||||||
|
assert len(SKILL_NAMES) == 8
|
||||||
|
assert {b["skill"] for b in DATA["blocks"]} == set(SKILL_NAMES)
|
||||||
|
|
||||||
|
|
||||||
|
# 2. No (a) block is left.
|
||||||
|
def test_no_duplicate_block_left():
|
||||||
|
for b in DATA["blocks"]:
|
||||||
|
assert b["class"] in ("b", "c"), b
|
||||||
|
assert b["kind"] in KINDS, b
|
||||||
|
for r in DATA["removed"]:
|
||||||
|
assert r["class"] == "a", r
|
||||||
|
|
||||||
|
|
||||||
|
# 3. A removed block is replaced by the entry-point call.
|
||||||
|
@pytest.mark.parametrize("removed", DATA["removed"], ids=lambda r: f"{r['skill']}:{r['base_start']}")
|
||||||
|
def test_removed_block_replaced_by_entry_call(removed):
|
||||||
|
assert (SKILLS / removed["script"]).is_file(), removed["script"]
|
||||||
|
assert removed["replacement"].startswith("bash .agents/skills/" + removed["script"])
|
||||||
|
bodies = [b[4] for b in parse_blocks(skill_text(removed["skill"]))]
|
||||||
|
assert any(removed["replacement"] in body for body in bodies), \
|
||||||
|
f"{removed['skill']}: no block contains {removed['replacement']!r}"
|
||||||
|
assert removed["script"] in DATA["entry_points"]
|
||||||
|
|
||||||
|
|
||||||
|
# 4. Exit-code tables do not contradict the code.
|
||||||
|
@pytest.mark.parametrize("script", sorted(DATA["entry_points"]))
|
||||||
|
def test_exit_table_agrees_with_script(script):
|
||||||
|
ep = DATA["entry_points"][script]
|
||||||
|
listed = ep["exit_codes"]
|
||||||
|
assert exit_table(ep["skill"]) == listed, f"{ep['skill']}/SKILL.md exit table rows != data"
|
||||||
|
assert listed[-1] == "other", "the table needs a catch-all row: under set -e a failing step's code passes through"
|
||||||
|
numeric = set(listed[:-1])
|
||||||
|
literal = literal_exit_codes(script)
|
||||||
|
assert literal <= numeric, f"{script} exits with {sorted(literal - numeric)} but the table lacks them"
|
||||||
|
passthrough = ep["passthrough"]
|
||||||
|
assert numeric <= literal | set(passthrough), \
|
||||||
|
f"table codes {sorted(numeric - literal - set(passthrough))} come from nowhere in {script}"
|
||||||
|
for code, ref in passthrough.items():
|
||||||
|
path, first, _ = _evidence_line(ref)
|
||||||
|
line = path.read_text(encoding="utf-8").split("\n")[first - 1]
|
||||||
|
assert re.search(rf"SystemExit\({code}\)|\bexit\s+{code}\b", line), f"{ref} does not raise {code}: {line!r}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_literal_exit_rule_on_base_scripts():
|
||||||
|
"""The counting rule: heredocs and comments skipped, so create's header `exit 4`
|
||||||
|
and stop's `sys.exit(7)` are not counted."""
|
||||||
|
assert literal_exit_codes("multi-agent-mux-resume/scripts/resume_session.sh") == {"0", "1", "2"}
|
||||||
|
assert literal_exit_codes("multi-agent-mux-stop/scripts/stop_session.sh") == {"0", "1", "2", "3", "4"}
|
||||||
|
assert literal_exit_codes("multi-agent-mux-create/scripts/create_session.sh") == {"0", "1", "2", "3", "42", "43"}
|
||||||
|
|
||||||
|
|
||||||
|
# 5. Evidence exists.
|
||||||
|
def test_evidence_points_into_existing_files():
|
||||||
|
for b in DATA["blocks"] + DATA["removed"]:
|
||||||
|
for ref in b["evidence"]:
|
||||||
|
path, first, last = _evidence_line(ref)
|
||||||
|
assert path.is_file(), ref
|
||||||
|
assert 1 <= first <= last <= len(path.read_text(encoding="utf-8").split("\n")), ref
|
||||||
|
|
||||||
|
|
||||||
|
# 6. The loading path is unchanged.
|
||||||
|
@pytest.mark.parametrize("skill", SKILL_NAMES)
|
||||||
|
def test_frontmatter_name_matches_directory(skill):
|
||||||
|
m = re.search(r"^name:\s*(\S+)\s*$", skill_text(skill).split("---")[1], re.M)
|
||||||
|
assert m and m.group(1) == skill
|
||||||
|
|
||||||
|
|
||||||
|
def test_claude_skills_link_resolves_to_agents_skills():
|
||||||
|
link = REPO / ".claude" / "skills"
|
||||||
|
if not link.exists():
|
||||||
|
pytest.skip(".claude/skills is untracked; absent in a worktree")
|
||||||
|
assert link.resolve() == SKILLS.resolve()
|
||||||
Reference in new issue
Block a user