docs(issue-42): stages 1-2 - SKILL.md code-block inventory; remove blocks the scripts already do #46

Merged
Godopu merged 5 commits from fix/issue-42 into main 2026-10-03 23:36:27 +00:00
5 changed files with 93 additions and 19 deletions
Showing only changes of commit 17eba0904e - Show all commits

No files matched your search

@@ -161,7 +161,7 @@ bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh --workspace
| `--role` | Required. Assigned role | | `--role` | Required. Assigned role |
| `--session` (`--name`) | herdr session name (default: `derive_session_name`) | | `--session` (`--name`) | herdr session name (default: `derive_session_name`) |
| `--wrapper` | claude only: use the `~/.local/bin/<session>` wrapper even if it is not present | | `--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 | | `--dry-run` | Print what would be spawned; no spawn and no registry (YAML/DB) update. The checks before it still run, and on macOS they remove the quarantine attribute from the agent binary |
| `--herdr-session` (`--herdr-server`) | Isolated herdr server name | | `--herdr-session` (`--herdr-server`) | Isolated herdr server name |
| `--herdr-workspace` | herdr workspace label recorded in the registry (a label only, never a socket) | | `--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 | | `--submit-job PROMPT` | Register a delegated job with this prompt and send it to the new agent |
@@ -176,7 +176,7 @@ bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh --workspace
| 2 | Bad arguments; control-plane binding refused for a worktree; whitespace in a control-plane path that must be forwarded | | 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) | | 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 | | 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 | | 42 | The TUI never became ready and the process is gone, or it is alive and `MAM_KILL_ON_READY_TIMEOUT=1` (rolled back) |
| 43 | The rollback could not kill the herdr session; it is still running | | 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 | | 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 |
+10 -6
View File
@@ -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** — `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>`. 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 --session <server> agent attach <name>`, where `<server>` is the row's `herdr_session` (the server it was resumed on).
### Resuming a `stopped` session (`stopped → running`) ### Resuming a `stopped` session (`stopped → running`)
@@ -60,7 +60,7 @@ If both are empty → for Class A agents (`agy`, `hermes`, `opencode`, `codex`),
bash .agents/skills/multi-agent-mux-resume/scripts/resume_session.sh --workspace <path> --agent <agent> --session <name> bash .agents/skills/multi-agent-mux-resume/scripts/resume_session.sh --workspace <path> --agent <agent> --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>`. `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 that names the server, because the server the script exports does not reach the calling shell and the native `herdr` binary does not route on environment variables: `herdr --session <server> agent attach <name>` (`<server>` = the `--herdr-session` you passed, or the row's `herdr_session`).
| Option | Meaning | | Option | Meaning |
|---|---| |---|---|
@@ -70,7 +70,7 @@ bash .agents/skills/multi-agent-mux-resume/scripts/resume_session.sh --workspace
| `--herdr-session` (`--herdr-server`) | Isolated herdr server name | | `--herdr-session` (`--herdr-server`) | Isolated herdr server name |
| `--herdr-workspace` | herdr workspace label | | `--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) | | `--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 | | `--dry-run` | Resolve the command and print it; no spawn and no registry (YAML/DB) update. The checks before it still run, and on macOS they remove the quarantine attribute from the agent binary |
| Exit | Meaning | | Exit | Meaning |
|---|---| |---|---|
@@ -101,8 +101,12 @@ A session is resumed only in the directory it ran in (its row's `pane.cwd`): the
## Verification ## Verification
```bash ```bash
# The server the session runs on (its row's herdr_session); the native herdr
# binary needs it as --session, an exported variable is not enough.
SERVER=$(python3 -c "import yaml; d = yaml.safe_load(open('.mam/agent-sessions.yaml')); print([s for s in d['herdr_sessions'] if s['name'] == '$SESSION_NAME'][0].get('herdr_session') or 'default')")
# 1. herdr alive with the right cmd (real native command, no lib.sh needed) # 1. herdr alive with the right cmd (real native command, no lib.sh needed)
herdr agent get "$SESSION_NAME" | python3 -c " herdr --session "$SERVER" agent get "$SESSION_NAME" | python3 -c "
import sys, json import sys, json
a = json.load(sys.stdin)['result']['agent'] a = json.load(sys.stdin)['result']['agent']
print(f\"cmd={a['agent']} cwd={a['cwd']}\") print(f\"cmd={a['agent']} cwd={a['cwd']}\")
@@ -119,7 +123,7 @@ print(f' pane.cmd_full: {s[\"pane\"][\"cmd_full\"]}')
# 3. TUI shows resumed conversation (real native command, no lib.sh needed) # 3. TUI shows resumed conversation (real native command, no lib.sh needed)
sleep 5 sleep 5
herdr agent read "$SESSION_NAME" --source visible --lines 30 herdr --session "$SERVER" 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)
``` ```
@@ -132,4 +136,4 @@ herdr agent read "$SESSION_NAME" --source visible --lines 30
- **No saved session yet** → `multi-agent-mux-create` - **No saved session yet** → `multi-agent-mux-create`
- **Killing an existing session** → `multi-agent-mux-stop` - **Killing an existing session** → `multi-agent-mux-stop`
- **Just attaching** → `herdr agent attach <name>` (no skill needed) - **Just attaching** → `herdr --session <server> agent attach <name>` (no skill needed; `<server>` = the row's `herdr_session`)
+1 -1
View File
@@ -58,7 +58,7 @@ bash .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \
| Exit | Meaning | | Exit | Meaning |
|---|---| |---|---|
| 0 | Stopped; or already `stopped` (no-op) | | 0 | Stopped; or already `stopped` (no-op, except with `--purge-conversation`, which still purges) |
| 1 | From the checks before the stop, nothing was changed: YAML not found, or the session is not registered. **The registry update after the stop also exits 1** (the row disappeared from the YAML during the run) | | 1 | From the checks before the stop, nothing was changed: YAML not found, or the session is not registered. **The registry update after the stop also exits 1** (the row disappeared from the YAML during the run) |
| 2 | Bad arguments (including the retired `--mode` / `--capture-id` / `--graceful`), or `--agent` omitted and not resolvable | | 2 | Bad arguments (including the retired `--mode` / `--capture-id` / `--graceful`), or `--agent` omitted and not resolvable |
| 3 | `--purge-conversation` without `--yes` | | 3 | `--purge-conversation` without `--yes` |
+27 -8
View File
@@ -339,9 +339,9 @@
{ {
"skill": "multi-agent-mux-resume", "skill": "multi-agent-mux-resume",
"start": 103, "start": 103,
"end": 124, "end": 128,
"lang": "bash", "lang": "bash",
"sha": "f4bddf5ba4d1", "sha": "8a468dfa43bd",
"class": "b", "class": "b",
"kind": "verification", "kind": "verification",
"evidence": [], "evidence": [],
@@ -492,7 +492,8 @@
], ],
"replacement": "bash .agents/skills/multi-agent-mux-resume/scripts/resume_session.sh --workspace <path> --agent <agent> --session <name>", "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", "script": "multi-agent-mux-resume/scripts/resume_session.sh",
"note": "Workflow re-wrote resume_session.sh in bash" "note": "Workflow re-wrote resume_session.sh in bash",
"base_fingerprint": "cd4000a4afae"
}, },
{ {
"skill": "multi-agent-mux-stop", "skill": "multi-agent-mux-stop",
@@ -511,7 +512,8 @@
], ],
"replacement": "bash .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh", "replacement": "bash .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh",
"script": "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" "note": "pre-flight (registered? already stopped?); replaced by one prose line, the Workflow block already calls the entry point",
"base_fingerprint": "f2e5380dbaa4"
}, },
{ {
"skill": "multi-agent-mux-create", "skill": "multi-agent-mux-create",
@@ -531,7 +533,8 @@
], ],
"replacement": "bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh --workspace <path> --agent <agent> --role <role>", "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", "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" "note": "pre-flight; replaced by prose, the has_real_herdr vs command -v pitfall kept as prose",
"base_fingerprint": "6caf3573a587"
}, },
{ {
"skill": "multi-agent-mux-create", "skill": "multi-agent-mux-create",
@@ -554,7 +557,8 @@
], ],
"replacement": "bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh --workspace <path> --agent <agent> --role <role>", "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", "script": "multi-agent-mux-create/scripts/create_session.sh",
"note": "Workflow re-wrote create_session.sh in bash" "note": "Workflow re-wrote create_session.sh in bash",
"base_fingerprint": "bc8eec474b36"
}, },
{ {
"skill": "multi-agent-mux-create", "skill": "multi-agent-mux-create",
@@ -569,7 +573,8 @@
"mismatch": [], "mismatch": [],
"replacement": "bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh --workspace <path> --agent <agent> --role <role>", "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", "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" "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",
"base_fingerprint": "b8ff0badf537"
} }
], ],
"entry_points": { "entry_points": {
@@ -620,5 +625,19 @@
"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)", "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)", "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)",
"R7 (review 08e1f1b2): stop's table said exit 1 is always pre-stop; the registry update after the kill chain also raises SystemExit(1) (stop_session.sh:310) when the row disappeared" "R7 (review 08e1f1b2): stop's table said exit 1 is always pre-stop; the registry update after the kill chain also raises SystemExit(1) (stop_session.sh:310) when the row disappeared"
],
"superseded": [
{
"skill": "multi-agent-mux-create",
"base_start": 263,
"base_end": 266,
"lang": "bash",
"base_sha": "ceb96a8c1293",
"class": "c",
"kind": "invocation",
"replacement": "bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh",
"note": "The base call example under 'Updating agent-sessions.yaml' (with the nonexistent agent-sessions-yaml-edit script above it, prose_mismatches C4) is folded into the Workflow call block; no information lost.",
"base_fingerprint": "b6a8a25279c5"
}
] ]
} }
+53 -2
View File
@@ -4,8 +4,10 @@ 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 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 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 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 inventory, when a deleted block's body comes back (by exact sha or by a
the entry script. whitespace-insensitive fingerprint, whatever the inventory says about it), or
when an exit-code table drifts from the entry script. A (c) block folded into
another call is recorded under "superseded".
""" """
import hashlib import hashlib
import json import json
@@ -127,6 +129,55 @@ def test_no_duplicate_block_left():
assert r["class"] == "a", r assert r["class"] == "a", r
def fingerprint(body):
"""A deleted body's identity that survives reindenting and reflowing."""
return hashlib.sha256(" ".join(body.split()).encode()).hexdigest()[:12]
def _returned_bodies(texts, gone):
"""(skill, start, which) for each current block whose body is a deleted one."""
hits = []
for skill, text in texts.items():
for start, _end, _lang, sha, body in parse_blocks(text):
for g in gone:
if sha == g["base_sha"] or fingerprint(body) == g["base_fingerprint"]:
hits.append((skill, start, f"{g['skill']}:{g['base_start']}"))
return hits
# 2b. A deleted body stays deleted - even when the inventory is updated to list
# it again under another class (review R3).
def test_no_deleted_body_came_back():
gone = DATA["removed"] + DATA["superseded"]
texts = {s: skill_text(s) for s in SKILL_NAMES}
assert _returned_bodies(texts, gone) == []
def test_a_returned_body_is_caught_reindented_and_with_the_inventory_updated():
"""The guard itself: a current block posing as a deleted one is found by its
exact sha and, reindented, by its fingerprint; listing it in the inventory
does not hide it (that check never reads DATA["blocks"])."""
skill = "multi-agent-mux-stop"
start, _end, _lang, sha, body = parse_blocks(skill_text(skill))[0]
fake = {"skill": skill, "base_start": 1, "base_sha": sha, "base_fingerprint": fingerprint(body)}
assert _returned_bodies({skill: skill_text(skill)}, [fake]) == [(skill, start, f"{skill}:1")]
reindented = "\n".join(" " + line for line in body.split("\n"))
other = {"skill": skill, "base_start": 1, "base_sha": "0" * 12, "base_fingerprint": fingerprint(reindented)}
assert _returned_bodies({skill: skill_text(skill)}, [other]) == [(skill, start, f"{skill}:1")]
# 2c. A deleted (c) block folded into another call is recorded too (review C1):
# with it the 37 base blocks are all accounted for.
def test_superseded_blocks_are_recorded_and_replaced():
assert DATA["superseded"], "the folded create call is recorded"
for r in DATA["superseded"]:
assert r["class"] == "c" and r["kind"] in KINDS, r
bodies = [b[4] for b in parse_blocks(skill_text(r["skill"]))]
assert any(r["replacement"] in body for body in bodies), r
for r in DATA["removed"] + DATA["superseded"]:
assert re.fullmatch(r"[0-9a-f]{12}", r["base_fingerprint"]), r
# 3. A removed block is replaced by the entry-point call. # 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']}") @pytest.mark.parametrize("removed", DATA["removed"], ids=lambda r: f"{r['skill']}:{r['base_start']}")
def test_removed_block_replaced_by_entry_call(removed): def test_removed_block_replaced_by_entry_call(removed):