From 7670b41afdf505ff912c4f598ada3a2a31ca1bd9 Mon Sep 17 00:00:00 2001 From: Godopu Date: Sat, 3 Oct 2026 13:42:08 +0000 Subject: [PATCH] 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 --- .../skills/multi-agent-mux-create/SKILL.md | 153 +---- .../skills/multi-agent-mux-resume/SKILL.md | 92 +-- .../skills/multi-agent-mux-status/SKILL.md | 43 +- .agents/skills/multi-agent-mux-stop/SKILL.md | 42 +- tests/data/skill_code_blocks.json | 621 ++++++++++++++++++ tests/test_issue42_skill_blocks.py | 176 +++++ 6 files changed, 901 insertions(+), 226 deletions(-) create mode 100644 tests/data/skill_code_blocks.json create mode 100644 tests/test_issue42_skill_blocks.py diff --git a/.agents/skills/multi-agent-mux-create/SKILL.md b/.agents/skills/multi-agent-mux-create/SKILL.md index 5ab3337..089c4a0 100644 --- a/.agents/skills/multi-agent-mux-create/SKILL.md +++ b/.agents/skills/multi-agent-mux-create/SKILL.md @@ -30,28 +30,9 @@ So `$WORKSPACE_ROOT/landing_page/refer_landing_page` + `claude` → `landing-pag ## 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 -# 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. +If a check fails, report the reason (automated path) or tell the user (interactive path). Do not proceed with a half-broken setup. ## Standard names @@ -163,109 +144,45 @@ herdr_sessions: kill_command: "HERDR_SESSION_NAME= herdr kill-session -t " ``` +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 ```bash -WORKSPACE=/path/to/project -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) +bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh --workspace --agent --role ``` +`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/` 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 -After spawn, append a new `herdr_sessions[]` entry to `.mam/agent-sessions.yaml`: - -```yaml -- name: - status: running - herdr_session_created_at: 2026-06-17T...Z # ISO 8601 UTC - herdr_session_epoch: - herdr_session: # Isolated session name (default: 'mam-') - herdr_server: # Alias for herdr_session - pane: - index: 0 - pid: - cmd: # 'claude' or 'agy' - cmd_full: - cwd: - tui: # only for claude - model: - provider: - plan: - account: - version: - start_command: "HERDR_SESSION_NAME= herdr new-session -d -s -x 140 -y 40 -c " - attach_command: "HERDR_SESSION_NAME= herdr agent attach " - kill_command: "HERDR_SESSION_NAME= herdr kill-session -t " - # 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. +`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. ## 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 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 diff --git a/.agents/skills/multi-agent-mux-resume/SKILL.md b/.agents/skills/multi-agent-mux-resume/SKILL.md index 9dd42a8..74194a0 100644 --- a/.agents/skills/multi-agent-mux-resume/SKILL.md +++ b/.agents/skills/multi-agent-mux-resume/SKILL.md @@ -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 ` / `agy --conversation `. 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//` (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 `. ### Resuming a `stopped` session (`stopped → running`) @@ -57,66 +57,30 @@ If both are empty → for Class A agents (`agy`, `hermes`, `opencode`, `codex`), ## Workflow ```bash -WORKSPACE=/path/to/project -AGENT=claude # claude | agy | hermes | grok | opencode | codex — pass it explicitly -SESSION_NAME=-creator- # 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 ; otherwise use -r -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" +bash .agents/skills/multi-agent-mux-resume/scripts/resume_session.sh --workspace --agent --session ``` +`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 `. + +| 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) 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. - **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. - **`agy --conversation ` will fail if the conversation was deleted from disk** — check `~/.gemini/antigravity-cli/conversations/.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) ``` -> `herdr list-panes` / `herdr capture-pane` here are tmux-compat pseudo-commands that only -> work after `source .agents/skills/lib.sh` (as done in `Workflow` above) — the real `herdr` -> binary doesn't have those subcommands. This block uses the real `herdr agent get`/`herdr agent read` -> equivalents instead so it also works standalone. +> `herdr list-panes` / `herdr capture-pane` are tmux-compat pseudo-commands that only +> work after `source .agents/skills/lib.sh` — the real `herdr` binary doesn't have those +> subcommands. This block uses only the real `herdr agent get`/`herdr agent read` commands, +> so it works standalone. ## When NOT to use this skill diff --git a/.agents/skills/multi-agent-mux-status/SKILL.md b/.agents/skills/multi-agent-mux-status/SKILL.md index 986a692..ce4636b 100644 --- a/.agents/skills/multi-agent-mux-status/SKILL.md +++ b/.agents/skills/multi-agent-mux-status/SKILL.md @@ -73,31 +73,40 @@ lab-paper-pdf2md-creator-claude default running alive clau ```json { - "yaml_path": "...", - "herdr_sessions_alive": ["..."], - "yaml_entries": [...], - "rows": [ + "timestamp": "2026-06-19T14:20:00Z", + "yaml_path": ".../.mam/agent-sessions.yaml", + "herdr_sessions_alive": ["lab-landing-page-creator-claude|default"], + "herdr_confirmed": true, + "drifts": [ + {"class": "A", "name": "lab-landing-page-creator-agy", "msg": "..."} + ], + "actions": [], + "sessions_detail": [ { "name": "lab-landing-page-creator-claude", - "yaml_status": "running", + "server": "default", + "herdr_workspace": "landing-page", + "status": "running", "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", - "resume_uuid_on_disk": true, - "drift": null - }, - { - "name": "lab-landing-page-creator-agy", - "yaml_status": "terminated", - "herdr_alive": false, - "drift": "yaml-says-terminated-but-disk-uuid-still-present" + "attach_command": "HERDR_SESSION_NAME=default herdr agent attach lab-landing-page-creator-claude", + "drift_classes": [], + "pane_pid": 12345, + "cmd_full": "claude --dangerously-skip-permissions --session-id ", + "start_command": "...", + "last_visible_status": "pinned" } - ], - "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) | Class | Detection | Meaning | diff --git a/.agents/skills/multi-agent-mux-stop/SKILL.md b/.agents/skills/multi-agent-mux-stop/SKILL.md index b54e034..4de1301 100644 --- a/.agents/skills/multi-agent-mux-stop/SKILL.md +++ b/.agents/skills/multi-agent-mux-stop/SKILL.md @@ -35,32 +35,7 @@ The stop command is always **graceful by default**: ## Pre-flight -```bash -SESSION_NAME=-creator- # 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 -``` +`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). ## 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) # — this prevents any future resume (status=terminated, resumable=false). 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. **`--agent` is the standard.** Pass it on every invocation. If omitted, the script diff --git a/tests/data/skill_code_blocks.json b/tests/data/skill_code_blocks.json new file mode 100644 index 0000000..cfd70c5 --- /dev/null +++ b/tests/data/skill_code_blocks.json @@ -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 --agent --session ", + "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 --agent --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 --agent --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 --agent --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)" + ] +} \ No newline at end of file diff --git a/tests/test_issue42_skill_blocks.py b/tests/test_issue42_skill_blocks.py new file mode 100644 index 0000000..785087e --- /dev/null +++ b/tests/test_issue42_skill_blocks.py @@ -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"(?