feat(issue-21): persist and forward the control-plane binding in create/resume/stop; status and reconcile honor MAM_JOBS_DIR

A session row now records the control plane it belongs to, and the pane
gets it explicitly instead of relying on env inheritance.

- lib_py/control_plane.py (+ thin lib.sh wrappers mam_control_plane_json /
  mam_control_plane_prefix): the binding is WORKSPACE_ROOT,
  AGENT_SESSIONS_YAML, MAM_JOBS_DIR, DELEGATE_JOB_LOGS_DIR as absolute paths.
- create: the binding is always persisted on the row (`control_plane`) and
  forwarded to the pane only when it differs from what --workspace alone
  implies, as leading K=V tokens that lib.sh's new-session already turns into
  --env (fresh pane) or `env K=V` (reused idle pane). pane.cmd_full and
  start_command stay unprefixed. A binding with whitespace that would have to
  be forwarded is refused with exit 2 (lib.sh splits the flags unquoted); the
  ~/.local/bin wrapper path records the binding but warns it is not forwarded.
- resume: the row's binding wins over the caller's env (a difference is
  reported on stderr) and is forwarded again; a legacy row without one uses
  the caller's binding and update_yaml_resumed.sh records it.
- stop: registry order MAM_JOBS_DIR > the row's mam_jobs_dir >
  $WORKSPACE_ROOT/.mam/jobs > .mam/jobs, exported for delegate_publish_event.
- status: both get_job_status copies look in MAM_JOBS_DIR, the row's
  registry and the matching audit logs before the old candidates.
- reconcile: MAM_JOBS_DIR wins in the subscribe heredoc and is passed in.

Tests T3-0..T3-8 (mock herdr: forwarding is verified at the herdr-call level
only; real pane env propagation is left to item 7). T3-7 is a static guard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
This commit is contained in:
GodopuandClaude Opus 5 committed 2026-09-14 03:34:48 +00:00
1 parent d907359746
commit ee1d8966c1
9 files changed
+409 -11

No files matched your search

@@ -202,6 +202,22 @@ if [ -z "$CMD_FULL" ]; then
esac
fi
# Issue #21 (item 3): the control-plane binding. Persisted on the row always;
# forwarded to the pane (leading K=V tokens that lib.sh's new-session turns
# into --env) only when it differs from what --workspace alone implies.
# lib.sh splits those flags unquoted (and the idle-pane path evals them), so a
# binding with whitespace that would have to be forwarded is refused.
CP_JSON="$(mam_control_plane_json "$WORKSPACE" 2>/dev/null || true)"
CP_PREFIX=""
if [ -n "$CP_JSON" ]; then
cp_rc=0
CP_PREFIX="$(mam_control_plane_prefix "$CP_JSON")" || cp_rc=$?
if [ "$cp_rc" -eq 3 ]; then
echo "ERROR: control-plane path contains whitespace; cannot forward to the pane" >&2
exit 2
fi
fi
spawn() {
if [ -z "$HERDR_SERVER_OPT" ]; then
if [ -z "${HERDR_SESSION_NAME:-}" ] || [ "$HERDR_SESSION_NAME" = "default" ]; then
@@ -213,20 +229,21 @@ spawn() {
if { [ -x "$WRAPPER" ] && [ "$(basename "$WRAPPER")" != "claude" ]; } || [ "$USE_WRAPPER" = "1" ]; then
SESSION_UUID=""
CMD_FULL="$("$(_delegate_py_bin)" -m lib_py.agents spawn-spec "$AGENT" "$RESOLVED_BIN" "" "true" 2>/dev/null || echo "${RESOLVED_BIN} --dangerously-skip-permissions")"
[ -z "$CP_PREFIX" ] || echo "WARNING: the ~/.local/bin wrapper path bypasses herdr; the control-plane binding is recorded but not forwarded" >&2
nohup "$WRAPPER" >/dev/null 2>&1 &
disown
else
HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL"
HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CP_PREFIX$CMD_FULL"
fi
;;
agy|hermes|grok|codex)
HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL"
HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CP_PREFIX$CMD_FULL"
;;
opencode)
if [ -z "${OPENCODE_PERMISSION:-}" ]; then
export OPENCODE_PERMISSION='{"*":"allow"}'
fi
HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CMD_FULL"
HERDR_SESSION_NAME="$HERDR_SESSION_NAME" _herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "$CP_PREFIX$CMD_FULL"
;;
*) echo "ERROR: --agent must be claude, agy, hermes, grok, opencode, or codex, got: $AGENT" >&2; exit 2 ;;
esac
@@ -391,7 +408,7 @@ atomic_dump_yaml "$AGENT_SESSIONS_YAML" \
MAM_WS_LABEL="$MAM_WS_LABEL" \
SESSION_UUID="$SESSION_UUID" \
LAST_VISIBLE_STATUS_OVERRIDE="${LAST_VISIBLE_STATUS_OVERRIDE:-}" \
DELEGATE_JOB_ID="$DELEGATE_JOB_ID" ROLE="$ROLE" <<'PYEOF'
DELEGATE_JOB_ID="$DELEGATE_JOB_ID" ROLE="$ROLE" CONTROL_PLANE_JSON="$CP_JSON" <<'PYEOF'
name = os.environ['SESSION_NAME']
agent = os.environ['AGENT']
role = os.environ['ROLE']
@@ -487,6 +504,15 @@ last_vis_override = os.environ.get('LAST_VISIBLE_STATUS_OVERRIDE', '')
if last_vis_override:
entry['last_visible_status'] = last_vis_override
# Issue #21: the control-plane binding this row was created against.
_cp_raw = os.environ.get('CONTROL_PLANE_JSON', '')
if _cp_raw:
try:
import json as _json
_cp = _json.loads(_cp_raw)
entry['control_plane'] = {k: _cp[k] for k in ('workspace_root', 'agent_sessions_yaml', 'mam_jobs_dir', 'delegate_job_logs_dir')}
except Exception:
pass
sessions.append(entry)
snap = d.setdefault('snapshot', {})