CI now shellchecks the update skill's scripts: SC1091 is disabled at their
`source` lines as in the other skills, and the two findings this branch added
(lib.sh's loop_lock.sh source, install.sh's one-item legacy loop) are
disabled at their lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A v4.11.0 workspace's .mam_deploy/update.sh installs the new version - the
skill present, no updater left - and the skill run after it converts the
registry. An update-backup in an unknown state now restarts the monitor it
stopped: nothing was changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The update is a skill (decision 4); deploy/update.sh and the installed
.mam_deploy/update.sh are gone.
- Phase 1 (installed code, update.sh): publish the update marker under the
exclusive admission lock; refuse before any change while a running row, a
live herdr agent in the workspace on any running server, an open job
(unreadable JSON included, dry_run not), a live loop marker or a
monitor.lock held by a monitor the skills did not start (U3) is there - the
calling agent (server + pane, its row and one running job) excepted
(lib_py/update.py precheck, decisions 1 and 7). Stop the monitor (TERM, then
KILL and restart it if it does not end; decision 2), handle a leftover
backup by its state (U4), back up .agents, .mam_deploy, root files, .venv,
the installer's .mam files and the registry (SQLite backup API under the
registry lock) into .mam/update-backup/, then exec a copy of
install_phase.sh outside the tree (decision 5).
- The copy (no lib.sh/lib_py, bash 3.2) runs the installer in place; on
failure it restores the backup and restarts the monitor from the restored
tree; on success it runs phase 2, post_update.sh of the new tree, which
converts the registry with lib_py.registry_format and restarts the monitor
if it ran. The backup and the marker are removed at the end.
- install.sh updates in place (decisions 3, 4, 9): install_state keys are set,
one line each; no updater is installed and an old one (.mam_deploy/update.sh,
or a root update.sh the manifest lists) is removed; framework files the new
version no longer ships are deleted when unmodified, kept and listed when
modified, and dropped from base/, the manifest and asset_hashes.txt; a local
source's .mam is not staged. remove.sh knows the new skill; CI shellchecks
its scripts; docs point at the skill.
- test_issue8_update_state and test_deploy_layout td6/td27 run the skill.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The update skill publishes .mam/update-in-progress (pid + lstart, the
loop_lock.sh rules) while it holds .mam/update-admission.lock exclusively.
create, resume, run_loop.sh and delegate submit (decision 8) and stop and
orc-onboard (U2: a rollback would restore the registry without their write)
take that lock shared (lib.sh mam_update_admit, fd 7), refuse while the marker
is live or unknown, and hold the lock until their record is written. A stale
marker refuses nothing. lib_py.pane_lock acquire gains --shared and
--wait-env (MAM_UPDATE_ADMISSION_WAIT_S, default 120); create's wrapper path
closes fd 7 for the long-lived child. The exit-1 rows of create/resume/stop
name the refusal; skill_code_blocks.json pins follow the shifted lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only create started the per-workspace monitor, and only on its job path, so
the stop -> update -> resume flow the update skill requires left no monitor.
resume now calls start_watchdog with the control root as workdir (as create
does since #21 A8, also for a worktree session) after the row is written, on
both the already-running and the spawn path. MAM_NO_SUBSCRIBER_DAEMON=1 starts
none; a failure warns and does not fail the resume.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
create's closing hint, the delegate-job wrapper's dispatch hint and stop's
stuck-TUI message printed the pre-#71 forms (`herdr session attach <name>`,
`HERDR_SESSION_NAME=... herdr agent attach <name>`), which miss an agent whose
name is over 32 characters or has upper case. They now print the session row's
attach_command, or lib_py.agents.sanitize.attach_command(server, name) when
there is no row (lib.sh session_attach_command). deploy/INSTALL.md and
install_mam.sh show the `herdr --session <server> agent attach <herdr name>`
form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A DB without its sessions table (or without both tables) was read as an
empty registry in strict mode, so stop closed a registered session
row-less and exited 0. Strict mode now fails on any OperationalError;
the non-strict reader is unchanged.
Refs #71
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- parity and create with real line breaks in the name;
- stop with malformed YAML (no DB) or a corrupt DB refuses (exit 1);
- test_layout: the shim heredoc anchor follows the { ... } > "$tmp_file"
form (the rule it checks is unchanged);
- S11: base re-pinned to 0fc162e, which changes the shim's sanitize body.
Refs #71
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
load_state_json printed {} on a YAML/DB read error, so a registered
session was closed row-less (exit 0) and its stop workflow skipped.
stop now reads the registry strictly and exits 1 before any herdr call.
Pins of the shifted stop_session.sh lines updated.
Refs #71
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The ASCII branch of _sanitize_herdr_agent_name translates line breaks
before sed (line by line) and $(...) (drops a trailing one) can lose
them: "newline-creator-claude\n" was started as newline-creator-claude
while its attach_command named newline-creator-claude-.
- `load_state_json strict` exits 1 (stderr) when the YAML or DB cannot be
read instead of printing {}; without the argument nothing changes.
Refs #71
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- create with a non-ASCII name: the attach target is the name herdr has;
bash/Python parity for non-ASCII and option-like names; no python3 is
an error for a non-ASCII name.
- row-less stop without --herdr-session uses HERDR_SESSION_NAME; an
unreadable row is not taken for no row.
- a retry (entry point, or any write) repairs a YAML a failed migration
left in format 1.
- tier3: the "cannot infer agent" case registers its row, as tier1 does.
- S11: base re-pinned to 5cd72d2, which changes the shim's sanitize body.
Refs #71
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The writer commits the migrated DB before it replaces the YAML. If that
replacement failed, the DB said format 2 and the next run did nothing.
When the DB is current the writer now also reads the YAML's format and
rewrites a YAML that is older or missing.
Refs #71
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With no --herdr-session the server is chosen as create_session.sh chose
it (HERDR_SESSION_NAME, else the workspace's), not always derived from
the workspace. The row-less path runs only when the lookup says the row
is missing (exit 7); any other lookup failure stops with exit 1 as
before. Pins of the shifted stop_session.sh lines updated.
Refs #71
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tr/sed lowercase differently from str.lower() outside ASCII, so a name
like "İstanbul-creator-claude" was started as x--stanbul-... while the
row's attach_command named i-stanbul-.... A non-ASCII name is now
lowercased and replaced by python3 (fail closed without it), and the
name is read with printf, not echo ("-n" no longer vanishes).
Refs #71
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- attach_command is `herdr --session <server> agent attach <herdr name>`
(`default` written too), built by lib_py.agents.sanitize.attach_command;
create, resume and reconcile all use it. kill_command and start_command
are no longer written, and status --json drops start_command.
- The two raw kill-session hints (create exit 43, id_order's orphan) print a
stop-skill command with --herdr-session instead.
- stop_session.sh closes a session that has no registry row, herdr only and
without --agent, when herdr runs it inside this workspace; another
workspace's session is refused (exit 5), a missing one is exit 1, one that
survives kill-session is exit 6. The registry is not changed.
- Docs: only the SKILL.md lines these changes made wrong; the skill block
pins follow. Tests that asserted the removed fields keep their intent
(issue50: create's own row; issue21: no control-root path in
attach_command; tier2: the new form and the absent fields). S11 pins the
shim to 4243da5, which changed it on purpose.
Refs #71
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- lib_py/registry_format.py: a pure, idempotent migrate() drops
kill_command/start_command and rebuilds attach_command on every row
(stopped/terminated included) and sets the top-level registry_format key.
- atomic_dump_yaml migrates an older registry under its lock, before the
caller's mutation, and then rewrites the YAML as well as the DB (the YAML
write is otherwise skipped when only fields change). load_state_json stays
read-only. New registries are written with the current version.
- Entry point for #72: `python -m lib_py.registry_format --yaml <path>` runs
the migration through the writer and prints
`registry-format: <from> -> <to>, rows changed: <n>`.
Refs #71
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
StallWatch converts --target once and hands the name to HerdrSource, so
agent get/read/prompt and agent wait cannot call herdr by different names.
Refs #71
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- One _sanitize_herdr_agent_name: the shim gets lib.sh's definition through
`declare -f` instead of a second copy. Without shasum/sha1sum/openssl it
hashes with python3 (same result as lib_py.agents.sanitize); with none of
them it fails instead of truncating or appending 00000000.
- Every caller stops on that failure: new-session before any pane is made,
the lookups report failure, and has-session takes the name lib.sh computed
(the cwd-relative import and its lower()[:32] fallback are gone).
- `herdr agent attach <name>` through the shim resolves the target like
get/read/prompt; options after the target are kept.
Refs #71
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
herdr registers an agent under sanitize_herdr_agent_name(session), so a
session name over 32 characters or with upper-case letters got
agent_not_found and stall detection went off. stall_watch now converts the
name for every herdr call (agent get/read/prompt, watcher.wait_once); logs
and notices keep the original name, and the agent kind is still read off
the original suffix (a long name's sanitized form ends in a hash).
The conftest mock herdr gets an opt-in strict_agent_names mode that, like
the real herdr, answers an agent only under its registered name and fails
get/read with agent_not_found JSON; existing tests see no change.
Refs #66
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A marker left on the invoking session name by a killed create/resume was
pruned before herdr was asked, so a retry under the same name took it over
while the orphaned pane was still alive (and create's failure cleanup could
then kill it). It is now kept and blocks like any other orphan until its
row is registered for the attempt or herdr reports the session gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR #69 review round 3 (rebuttal overruled; codex r2 applied as written).
- A failed `agent list`, herdr's server_not_running included, is unknown and
keeps an orphaned marker; only a successful list without the name prunes.
- No probe floor: probes get only the time left until the wait deadline, so
MAM_ID_ORDER_WAIT_S=0 asks herdr nothing (unknown, kept) and the refusal
says so.
- The timeout message reports the last definite answer of this wait, so an
alive orphan whose final probe ran out of time still reads as alive.
- Tests follow: server_not_running and cap 0 keep the marker and refuse;
delayed-probe bound is the cap itself; probing tests use a small cap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR #69 review round 2.
- The herdr probe is tri-state (present / absent / unknown) from a parsed
`_herdr agent list`: absent only when the list parses without the name or
herdr answers server_not_running. A failed, unparseable or timed-out lookup
is unknown and keeps the marker (codex P1: has-session's exit 1 meant both
"absent" and "lookup failed").
- Probes run with MAM_NO_AUTOSTART=1 so a check never starts a server (agy P2).
- Probe time is capped by max(deadline, start + PROBE_FLOOR_S=2 s), shared by
all markers, so a slow herdr cannot hold the wait or its lock past the cap
(codex P2); MAM_ID_ORDER_WAIT_S=0 still gets one bounded look.
- Tests: unknown probe keeps and refuses; delayed probe at caps 0/1/3 stays
within the bound; real probe via lib.sh mam_id_order_wait against mock herdr
(listed / unlisted / failing list / server_not_running).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A create/resume killed after spawning its pane but before registering its
row left a marker whose owner pid was dead; the next same-cwd waiter pruned
it on the pid alone and could start a second session that discovers the
same workspace conversation id (PR #69 review P1).
- Markers record the herdr server. A dead owner's marker is pruned only once
its row is registered for the attempt (running, newer epoch: the row
decides) or `_herdr has-session` reports the session gone; an unanswered
probe counts as alive. On timeout the kill-session command is printed.
- A live marker of another process for the waiter's own session name is kept
and blocks instead of being deleted; the waiter's own pid's markers go.
- Tests: orphan / registered / gone / same-session / missing pane pid cases,
and a create-level reproduction against mock herdr (fails before the fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- workspace_uuid.py: the unused sys import.
- stop_session.sh: the orphan CAPTURE_ID and the blank padding that kept
later line numbers; the inventory pins follow the move instead.
- lib.sh: capture_conversation_id, which nothing calls any more.
- resume SKILL.md: the flow names the session-id decision (session_ids.py),
not resolve_session_id.sh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The skill-block inventory recorded line numbers that the auto-pin change moved:
one create block, two monitor blocks, and the reconcile.sh evidence for the
drift A example (601 -> 593) and the status --json example (1084, past the new
end of the file -> 816).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
- BaseAgentAdapter: id_lifecycle (pre_assigned: claude, grok / post_discovered)
and capture_active_session_id - exactly one verified candidate newer than the
session that no other row records, else a reason (no-candidate, ambiguous, ...).
- agy: verify_artifact (discover) rejects an id last_conversations.json maps to
another workspace and always excludes ids other rows record (a cache hit no
longer bypasses it). capture uses the cache only as a proposal: the
conversation's presence lock or DB must be held by the row's pane process or
a descendant (lsof/ps); anything unprovable is not pinned.
- lib_py/agents/autopin.py: handle_completed, run by the side that verified the
job's completed - the delegate-job wrapper (direct), create --onboard and the
loop's wait_for_job (recovery, as a subprocess: G9). Local herdr jobs only;
one (job, dispatch token, attempt) is handled once (job-scoped flock + marker
in the job dir); writes go through atomic_dump_yaml; a pin records
session_id_source=discovered / session_id_verified=true; a failure leaves the
reason on the row and never changes the job's result.
- reconcile.sh drift C for agy, hermes, opencode and codex calls the same
capture (the agy *.db glob is gone); _pin_and_verify_resume uses pin_row.
- Tests: tests/test_issue40_auto_pin.py; issue64 :407 now expects False; the
static reconcile-source checks follow the move to the adapters; the two tier2
drift-C component tests create with --no-onboard (onboarding now pins).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>