Compare commits
3 Commits
feat/feder
...
enhance/tm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9c07cdeba | ||
| 851c67c27b | |||
| 86e106fcc9 |
33
docs/scratchpads/561-python-is-python3.md
Normal file
33
docs/scratchpads/561-python-is-python3.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Issue #561 — Bare python on agent hosts
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Make the durable bootstrap/provisioning guidance ensure agent hosts provide a bare `python` command that resolves to Python 3.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Add Debian/Ubuntu `python-is-python3` to agent-host prerequisites in bootstrap docs.
|
||||||
|
- Check for actual OS package provisioning scripts and update only if an existing agent-host package install path exists.
|
||||||
|
- Do not touch live host state.
|
||||||
|
- Do not update `docs/TASKS.md`; repo guidance says workers read it but never modify it.
|
||||||
|
|
||||||
|
## Recon
|
||||||
|
|
||||||
|
- Issue #561 confirms repeated `python: command not found` failures from fleet agents that emit `python foo.py`.
|
||||||
|
- `guides/BOOTSTRAP.md` and `packages/mosaic/framework/guides/BOOTSTRAP.md` are the source and packaged framework copies of the bootstrap guide.
|
||||||
|
- Targeted repo sweep found no agent-host Debian package provisioning script. Existing `apt-get install` hits are CI/test helper paths or unrelated deployment docs.
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. Add a host prerequisite section to both bootstrap guide copies.
|
||||||
|
2. Include `python-is-python3` in the Debian/Ubuntu package list with an issue comment.
|
||||||
|
3. Note the non-Debian equivalent as a `/usr/bin/python -> python3` symlink.
|
||||||
|
4. Validate markdown/diff, run shell syntax checks where applicable, run required review, commit, queue guard, and push.
|
||||||
|
|
||||||
|
## Validation Log
|
||||||
|
|
||||||
|
- `rg` recon: no existing agent-host Debian package provisioning script; only CI/test helper `apt-get install` paths and unrelated deployment docs.
|
||||||
|
- `git diff --check`: passed.
|
||||||
|
- `bash -n packages/mosaic/framework/install.sh tools/install.sh packages/mosaic/framework/tools/bootstrap/init-project.sh packages/mosaic/framework/tools/_scripts/mosaic-bootstrap-repo`: passed. No touched shell scripts.
|
||||||
|
- `~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted`: approved, 0 findings.
|
||||||
|
- `pnpm format:check`: initially blocked because `node_modules` was absent and `prettier` was unavailable; `pnpm install --frozen-lockfile` initially hit an invalid `/root` pnpm store path. Reran install with `--store-dir /home/hermes/agent-work/.pnpm-store`, then `pnpm format:check` passed.
|
||||||
@@ -15,6 +15,22 @@ This guide covers how to bootstrap a project so AI agents (Claude, Codex, etc.)
|
|||||||
7. Branching/merging is consistent: `branch -> main` via PR with squash-only merges
|
7. Branching/merging is consistent: `branch -> main` via PR with squash-only merges
|
||||||
8. Steered-autonomy execution is enabled so agents can run end-to-end with escalation-only human intervention
|
8. Steered-autonomy execution is enabled so agents can run end-to-end with escalation-only human intervention
|
||||||
|
|
||||||
|
## Agent Host Prerequisites
|
||||||
|
|
||||||
|
Agent hosts must provide the Python runtime shape that runtime agents and
|
||||||
|
Mosaic automation assume is present.
|
||||||
|
|
||||||
|
For Debian/Ubuntu hosts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
# #561: bare python invocations from agents must resolve.
|
||||||
|
sudo apt-get install -y python3 python-is-python3
|
||||||
|
```
|
||||||
|
|
||||||
|
For non-Debian hosts, install the equivalent Python 3 runtime and ensure
|
||||||
|
`/usr/bin/python` resolves to `python3` (for example, via a managed symlink).
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -15,6 +15,22 @@ This guide covers how to bootstrap a project so AI agents (Claude, Codex, etc.)
|
|||||||
7. Branching/merging is consistent: `branch -> main` via PR with squash-only merges
|
7. Branching/merging is consistent: `branch -> main` via PR with squash-only merges
|
||||||
8. Steered-autonomy execution is enabled so agents can run end-to-end with escalation-only human intervention
|
8. Steered-autonomy execution is enabled so agents can run end-to-end with escalation-only human intervention
|
||||||
|
|
||||||
|
## Agent Host Prerequisites
|
||||||
|
|
||||||
|
Agent hosts must provide the Python runtime shape that runtime agents and
|
||||||
|
Mosaic automation assume is present.
|
||||||
|
|
||||||
|
For Debian/Ubuntu hosts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
# #561: bare python invocations from agents must resolve.
|
||||||
|
sudo apt-get install -y python3 python-is-python3
|
||||||
|
```
|
||||||
|
|
||||||
|
For non-Debian hosts, install the equivalent Python 3 runtime and ensure
|
||||||
|
`/usr/bin/python` resolves to `python3` (for example, via a managed symlink).
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ message crosses the wire as base64 (`-b`) to avoid all shell-quoting hazards.
|
|||||||
|
|
||||||
- `agent-send.sh` — inter-agent wrapper (preamble + local/remote dispatch).
|
- `agent-send.sh` — inter-agent wrapper (preamble + local/remote dispatch).
|
||||||
- `send-message.sh` — low-level reliable single-pane submitter (`-b` base64 input).
|
- `send-message.sh` — low-level reliable single-pane submitter (`-b` base64 input).
|
||||||
|
- `auto-submit-drafts.sh` — watchdog that flushes stable unsubmitted prompt
|
||||||
|
drafts on a coordinator pane (default target `mos-claude`); run it as a
|
||||||
|
long-lived process alongside the coordinator session.
|
||||||
|
- `agent-send.test.sh` — regression + grammar lock for `agent-send.sh`.
|
||||||
- `test-send-message-socket.sh` — smoke test for named-socket isolation.
|
- `test-send-message-socket.sh` — smoke test for named-socket isolation.
|
||||||
|
|
||||||
## Distribution
|
## Distribution
|
||||||
|
|||||||
80
packages/mosaic/framework/tools/tmux/auto-submit-drafts.sh
Executable file
80
packages/mosaic/framework/tools/tmux/auto-submit-drafts.sh
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# auto-submit-drafts.sh — watchdog for Claude Code panes that receive channel
|
||||||
|
# messages but leave them as unsubmitted prompt drafts. Intended for Mos only.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
TARGET="${1:-mos-claude}"
|
||||||
|
INTERVAL="${INTERVAL:-2}"
|
||||||
|
STABLE_SECONDS="${STABLE_SECONDS:-4}"
|
||||||
|
LOG_PREFIX="[auto-submit-drafts:$TARGET]"
|
||||||
|
|
||||||
|
last_prompt=""
|
||||||
|
first_seen=0
|
||||||
|
|
||||||
|
prompt_text() {
|
||||||
|
tmux capture-pane -t "$TARGET" -p 2>/dev/null | python3 -c '
|
||||||
|
import sys, re
|
||||||
|
lines = sys.stdin.read().splitlines()
|
||||||
|
idx = None
|
||||||
|
for i in range(len(lines)-1, -1, -1):
|
||||||
|
if "❯" in lines[i]:
|
||||||
|
idx = i
|
||||||
|
break
|
||||||
|
if idx is None:
|
||||||
|
raise SystemExit
|
||||||
|
parts = []
|
||||||
|
after = lines[idx].split("❯", 1)[1]
|
||||||
|
parts.append(after)
|
||||||
|
for line in lines[idx+1:]:
|
||||||
|
# Stop at Claude Code separator/border lines.
|
||||||
|
if "─" in line or "╰" in line or "╭" in line:
|
||||||
|
break
|
||||||
|
s = line.replace("\u00a0", " ")
|
||||||
|
s = re.sub(r"[\x00-\x1f\x7f]", "", s).strip()
|
||||||
|
if s:
|
||||||
|
parts.append(s)
|
||||||
|
text = " ".join(parts).replace("\u00a0", " ")
|
||||||
|
text = re.sub(r"[\x00-\x1f\x7f]", "", text).strip()
|
||||||
|
print(text)
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if ! tmux has-session -t "$TARGET" 2>/dev/null; then
|
||||||
|
echo "$LOG_PREFIX target missing; waiting" >&2
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
last_prompt=""
|
||||||
|
first_seen=0
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
current="$(prompt_text || true)"
|
||||||
|
now="$(date +%s)"
|
||||||
|
|
||||||
|
if [[ -z "$current" ]]; then
|
||||||
|
last_prompt=""
|
||||||
|
first_seen=0
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$current" != "$last_prompt" ]]; then
|
||||||
|
last_prompt="$current"
|
||||||
|
first_seen="$now"
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
age=$(( now - first_seen ))
|
||||||
|
if (( age >= STABLE_SECONDS )); then
|
||||||
|
echo "$LOG_PREFIX submitting stable draft after ${age}s: ${current:0:120}" >&2
|
||||||
|
tmux send-keys -t "$TARGET" C-j
|
||||||
|
sleep 0.8
|
||||||
|
tmux send-keys -t "$TARGET" C-m
|
||||||
|
sleep 2
|
||||||
|
last_prompt=""
|
||||||
|
first_seen=0
|
||||||
|
else
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -77,10 +77,20 @@ snippet=$(printf '%s' "$MSG" | tr '\n' ' ' | tr -s ' ' | sed 's/[^[:print:]]//g'
|
|||||||
|
|
||||||
# 1) Paste the body as a bracketed paste so multi-line content does not submit
|
# 1) Paste the body as a bracketed paste so multi-line content does not submit
|
||||||
# line-by-line. load-buffer/paste-buffer is far safer than `send-keys -l`.
|
# line-by-line. load-buffer/paste-buffer is far safer than `send-keys -l`.
|
||||||
printf '%s' "$MSG" | "${tmux_cmd[@]}" load-buffer -b __mosaic_send -
|
# Buffer name MUST be unique per invocation: concurrent senders on the shared
|
||||||
|
# tmux server race a fixed name (load overwrites load, -d deletes underneath),
|
||||||
|
# cross-delivering or dropping messages — bit the fleet on the 2026-07-09
|
||||||
|
# simultaneous restart (briefs swapped between sessions).
|
||||||
|
BUF="__mosaic_send_$$_$(date +%s%N)"
|
||||||
|
printf '%s' "$MSG" | "${tmux_cmd[@]}" load-buffer -b "$BUF" -
|
||||||
# -p = bracketed paste when the client supports it; fall back if not.
|
# -p = bracketed paste when the client supports it; fall back if not.
|
||||||
"${tmux_cmd[@]}" paste-buffer -d -p -b __mosaic_send -t "$EFFECTIVE_TARGET" 2>/dev/null \
|
"${tmux_cmd[@]}" paste-buffer -d -p -b "$BUF" -t "$EFFECTIVE_TARGET" 2>/dev/null \
|
||||||
|| "${tmux_cmd[@]}" paste-buffer -d -b __mosaic_send -t "$EFFECTIVE_TARGET"
|
|| "${tmux_cmd[@]}" paste-buffer -d -b "$BUF" -t "$EFFECTIVE_TARGET" \
|
||||||
|
|| "${tmux_cmd[@]}" delete-buffer -b "$BUF" 2>/dev/null
|
||||||
|
# ^ -d deletes the buffer only on a SUCCESSFUL paste; if both attempts fail
|
||||||
|
# (e.g. the target vanished since the liveness check), delete explicitly —
|
||||||
|
# named buffers are exempt from tmux's buffer-limit eviction, so orphans
|
||||||
|
# would otherwise accumulate forever.
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
|
|
||||||
# 2) Submit, then verify; flush with another Enter if it is still a draft.
|
# 2) Submit, then verify; flush with another Enter if it is still a draft.
|
||||||
|
|||||||
@@ -47,4 +47,32 @@ if capture_default | grep -qF "agent socket hello"; then
|
|||||||
fail "agent-send.sh leaked named-socket message to default tmux server"
|
fail "agent-send.sh leaked named-socket message to default tmux server"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Concurrency: parallel senders on one server must not cross-deliver or drop.
|
||||||
|
# Locks the unique-per-invocation paste buffer (a fixed buffer name raced:
|
||||||
|
# load overwrote load, -d deleted underneath — messages swapped between panes).
|
||||||
|
CONC_N=5
|
||||||
|
for i in $(seq 1 "$CONC_N"); do
|
||||||
|
tmux -L "$SOCKET" new-session -d -s "conc-$i" -c "$TMPDIR" 'bash --noprofile --norc -i'
|
||||||
|
done
|
||||||
|
pids=()
|
||||||
|
for i in $(seq 1 "$CONC_N"); do
|
||||||
|
"$SEND_MESSAGE" -L "$SOCKET" -t "=conc-$i" -m "CONCPAYLOAD-${i}-END" >/dev/null &
|
||||||
|
pids+=($!)
|
||||||
|
done
|
||||||
|
for pid in "${pids[@]}"; do
|
||||||
|
wait "$pid" || fail "concurrent send-message.sh invocation exited non-zero"
|
||||||
|
done
|
||||||
|
sleep 0.2
|
||||||
|
for i in $(seq 1 "$CONC_N"); do
|
||||||
|
pane=$(tmux -L "$SOCKET" capture-pane -t "=conc-$i:0.0" -p)
|
||||||
|
printf '%s' "$pane" | grep -qF "CONCPAYLOAD-${i}-END" \
|
||||||
|
|| fail "concurrent send dropped payload for pane conc-$i"
|
||||||
|
for j in $(seq 1 "$CONC_N"); do
|
||||||
|
[ "$j" = "$i" ] && continue
|
||||||
|
if printf '%s' "$pane" | grep -qF "CONCPAYLOAD-${j}-END"; then
|
||||||
|
fail "concurrent send cross-delivered payload $j to pane conc-$i"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
echo "ok - named tmux socket send tools"
|
echo "ok - named tmux socket send tools"
|
||||||
|
|||||||
Reference in New Issue
Block a user