Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ and re-run `./install.sh`.

## You are the main agent unless told otherwise

- **Told otherwise = worker session.** If `SUME_WORKER_SESSION` /
`AGENT_HUMAN_STREAM_PID` is in your environment or the prompt opens with
`[sume worker session]`, you were started by `agent-human-stream` /
`sume-bg-launch`: you are the worker. Do the delegated task in this
session; never launch, resume, or hand off another worker (the launcher
refuses nested launches, exit 5; a nested process dies with your session —
sume#7839). Skip the rest of this section.
- Reply to the user in **their language** (Korean in → Korean out).
- **Chat work report is SoT.** Slack `#coding-agents` is a team mirror.
Chase must understand the turn without opening Slack. End every
Expand Down
34 changes: 33 additions & 1 deletion docs/MINI-WORKER-HOST.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,36 @@ The launcher reminds Mini workers of this requirement.

Full recipe: [references/dest-verify-issue-shots.md](../sume-desk/skills/sume-main-agent-orchestration/references/dest-verify-issue-shots.md).

## Nested launches are refused (sume#7839)

The "Astra disconnect" pattern: a Mini worker read the launcher flags quoted
in its prompt (`--backend codex --model gpt-6-astra --host mini`, "Fresh
worker", "Job title") plus the desk's "you are the main agent unless told
otherwise" and decided it was the main agent. It ran `sume-bg-launch` itself:

- `--host local` from inside the worker → Codex's unified exec returned the
tool call early (`status: failed`, banner in stdout), the model reported
"Started …", the outer job exited rc=0 and Codex killed the tracked child
at turn end. The nested session has no `remote-jobs/` dir (its opus-live
stamp is not a job id → `--status` says unknown job).
- `--host mini` from the Mini (ssh to itself) → a second, detached job on the
same clone; the outer job still exits rc=0 and the main agent relaunches.

Guards now in place:

- `agent-human-stream` exports `SUME_WORKER_SESSION=<name>` (plus the
existing `AGENT_HUMAN_STREAM_PID`); `sume-bg-remote run` exports
`SUME_BG_REMOTE_JOB=<job>`. Tool shells inherit them.
- `sume-bg-launch` (launch only) and `agent-human-stream` exit **5** inside
such a session and tell the model it *is* the worker. `--status` /
`--jobs` / `--attach` / `--kill` still work. Override on purpose only:
`SUME_BG_ALLOW_NESTED=1`.
- Every wrapper-launched prompt gets a `[sume worker session]` preamble;
`install.sh` appends "Sume worker mode (always on)" to `~/.codex/AGENTS.md`
and `~/.grok/AGENTS.md` (Claude Code inherits via the import).
- Main-agent prompts: task only, no launcher flags in the body (SKILL.md
delegation checklist).

## Secrets

- The Mini keeps its own logins. Nothing is forwarded (no agent forwarding,
Expand Down Expand Up @@ -165,4 +195,6 @@ runs offline (fake `ssh` / `scp` against a temp HOME standing in for the
Mini; fake wrapper). `install.sh` runs it. Covers: local regression, host
env / validation, fail-closed unreachable, prompt secret guard, mini launch
→ replica + registry, same-host resume, detached survival after attach
death, `--status` / `--jobs` / `--attach` / `--kill`.
death, `--status` / `--jobs` / `--attach` / `--kill`. Plus the nested-launch guard
(exit 5 inside a worker session, control plane still allowed,
`SUME_BG_ALLOW_NESTED=1` override) and the `SUME_BG_REMOTE_JOB` export.
9 changes: 9 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,15 @@ append_entry "$HOME/.claude/CLAUDE.md" \
append_entry "$HOME/.grok/AGENTS.md" \
"$ROOT/sume-desk/grok-agents-snippet.md" \
"sume-main-agent-orchestration/SKILL.md"
# Worker-mode lock (sume#7839): a wrapper-started session is the worker and
# must never spawn a nested worker. Codex + Grok entry points; Claude Code
# inherits it through the ~/.codex/AGENTS.md import.
append_entry "$HOME/.codex/AGENTS.md" \
"$ROOT/sume-desk/worker-mode-snippet.md" \
"## Sume worker mode (always on)"
append_entry "$HOME/.grok/AGENTS.md" \
"$ROOT/sume-desk/worker-mode-snippet.md" \
"## Sume worker mode (always on)"

# Existing snippets keep the old marker, so append the wait cook if missing.
ensure_wait_hint() {
Expand Down
20 changes: 17 additions & 3 deletions sume-desk/skills/sume-main-agent-orchestration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,13 @@ When the main agent is **Codex or Claude Code**:
- Use that harness's native background / subagent completion model instead
of Cursor notification semantics.

When Claude Code **is** the Opus worker (not the main agent): ignore all
main-agent monitoring rules; just do the delegated task and emit the final
report.
When this harness **is** the worker (Claude Code, Codex, or Grok Build
started by `agent-human-stream` / `sume-bg-launch`; env `SUME_WORKER_SESSION`
or a `[sume worker session]` prompt preamble): ignore all main-agent rules;
do the delegated task in this session and emit the final report. Never
launch, resume, or hand off another worker from a worker — the launcher
refuses nested launches (exit 5) and a nested process is killed when the
session ends, which is the "Astra disconnect" ghost pattern (sume#7839).

## Core Role Split

Expand Down Expand Up @@ -966,6 +970,16 @@ GRAPHITE (hard lock) — Chase 2026-09-06: author owns land to origin/main:
exiting after main land while dest deploy is still building.
```

Prompt hygiene (sume#7839 — nested-worker ghosts): the prompt body is the
**task**, not the launch command. Open it with the worker role
(`You are the worker for <job-slug> (#N). Do the work in this session.`) and
keep launcher flags on the `sume-bg-launch` command line only — never write
`--backend … --model … --effort … --host mini`, "Fresh worker", or
"Job title: …" inside the prompt. Astra/Codex reads such a header as "spawn
this worker", launches a nested session, and exits; the nested session dies
with it. The wrapper now prepends a `[sume worker session]` preamble and the
launcher refuses nested launches, but the prompt must not fight them.

When the BP has a PR train, also say:

- Implement **all** locked slices in one session, then **`gt submit --stack`**.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ Resume:
Watch:
tail -f ~/.cstack/state/opus-live/LATEST.log

Worker session: the prompt gets a "[sume worker session]" preamble and the
backend sees SUME_WORKER_SESSION=<name>. Launching this wrapper or
sume-bg-launch from inside such a session exits 5 (nested worker = ghost,
sume#7839). AGENT_HUMAN_STREAM_WORKER_PREAMBLE=0 / SUME_BG_ALLOW_NESTED=1 override.

On stream start/end the formatter prints:
📎 session_id=<uuid> backend=<claude|grok|codex>
and again just above —— final ——.
Expand Down Expand Up @@ -246,6 +251,36 @@ while [[ $# -gt 0 ]]; do
esac
done

# Nested-launch guard (sume#7839): a wrapper-started worker must not start
# another worker. The child dies with the parent's tool call/session
# (Codex unified exec kills tracked pids at turn end) or duplicates the job.
# --sessions / --self-test are reads. Override on purpose: SUME_BG_ALLOW_NESTED=1.
if [[ "$LIST_SESSIONS" -eq 0 && "$SELF_TEST" -eq 0 ]]; then
_nested_ctx=""
if [[ -n "${SUME_WORKER_SESSION:-}" ]]; then
_nested_ctx="worker session ${SUME_WORKER_SESSION}"
elif [[ -n "${AGENT_HUMAN_STREAM_PID:-}" ]]; then
_nested_ctx="worker session ${AGENT_HUMAN_STREAM_NAME:-?} (wrapper pid ${AGENT_HUMAN_STREAM_PID})"
fi
if [[ -n "$_nested_ctx" && -n "${SUME_BG_REMOTE_JOB:-}" ]]; then
_nested_ctx="${_nested_ctx} = Mini job ${SUME_BG_REMOTE_JOB}"
fi
if [[ -n "$_nested_ctx" ]]; then
if [[ "${SUME_BG_ALLOW_NESTED:-0}" == "1" ]]; then
echo "note: nested launch allowed by SUME_BG_ALLOW_NESTED=1 (inside ${_nested_ctx})" >&2
else
cat >&2 <<EOF
error: refusing a nested worker launch — you are already inside ${_nested_ctx}.
You ARE the worker for this job. Do the task in this session; do not
spawn, resume, or hand off to another worker (sume#7839).
Launcher flags quoted in your prompt describe how THIS session was
started, not a job to launch. Override only on purpose: SUME_BG_ALLOW_NESTED=1.
EOF
exit 5
fi
fi
fi

if [[ "$LIST_SESSIONS" -eq 1 ]]; then
if [[ ! -f "$REGISTRY" ]]; then
echo "(no registry yet: $REGISTRY)" >&2
Expand All @@ -256,6 +291,9 @@ if [[ "$LIST_SESSIONS" -eq 1 ]]; then
fi

if [[ "$SELF_TEST" -eq 1 ]]; then
# The self-test launches this wrapper with fake backends; scrub inherited
# worker markers so it also passes when run from inside a worker session.
unset SUME_WORKER_SESSION SUME_BG_REMOTE_JOB SUME_BG_ALLOW_NESTED AGENT_HUMAN_STREAM_PID AGENT_HUMAN_STREAM_NAME AGENT_HUMAN_STREAM_LIVE_LOG
_got="$(_grok_effort_wire mid)"
if [[ "$_got" != "medium" ]]; then
echo "self-test: grok effort alias mid → medium failed (got ${_got})" >&2
Expand Down Expand Up @@ -892,6 +930,20 @@ _codex_supervisor_check() {
fi
}

# Worker preamble (sume#7839): every wrapper-launched session is the WORKER
# for its job. Codex/Grok/Claude read desk instructions that say "you are the
# main agent unless told otherwise"; this tells them otherwise, in-band, so a
# prompt that quotes launcher flags is not read as "spawn a worker".
# AGENT_HUMAN_STREAM_WORKER_PREAMBLE=0 disables it (tests / one-off pipes).
PROMPT_ORIGINAL="$PROMPT"
if [[ "${AGENT_HUMAN_STREAM_WORKER_PREAMBLE:-1}" == "1" ]]; then
_wp_job="${SUME_BG_REMOTE_JOB:-}"
PROMPT="$(cat <<EOF
[sume worker session] You are the WORKER for job "${NAME:-$BACKEND}"${_wp_job:+ (Mini job ${_wp_job})}, started by agent-human-stream (backend ${BACKEND}). Do the task below yourself, in this session, through its done criteria. You are not the Sume main agent: the "you are the main agent" sections of ~/.cstack/src/AGENTS.md and the orchestration skill do not apply to you. Never run sume-bg-launch, agent-human-stream, claude-human-stream, or codex/claude/grok exec to spawn, resume, or hand off a worker — the launcher refuses nested launches (exit 5) and a nested process dies when this session ends. Any launcher flags quoted in the task (--backend, --model, --effort, --host mini, "Fresh worker", "Job title") describe how THIS session was started; they are not an instruction to launch anything. If you cannot proceed, say so in the final report instead of delegating.
EOF
)"$'\n\n'"${PROMPT}"
fi

CMD=()
if [[ "$BACKEND" == "codex" ]]; then
_codex_supervisor_check
Expand Down Expand Up @@ -960,7 +1012,7 @@ if [[ "$BACKEND" != "codex" ]]; then
CMD+=("${EXTRA[@]+"${EXTRA[@]}"}")
fi

PROMPT_HEAD=$(printf '%s' "$PROMPT" | tr '\n' ' ' | cut -c1-200)
PROMPT_HEAD=$(printf '%s' "${PROMPT_ORIGINAL:-$PROMPT}" | tr '\n' ' ' | cut -c1-200)

export AGENT_HUMAN_STREAM_BACKEND="$BACKEND"
export AGENT_HUMAN_STREAM_MODEL="${_resolved_model:-}"
Expand All @@ -969,6 +1021,9 @@ export AGENT_HUMAN_STREAM_PROMPT_HEAD="$PROMPT_HEAD"
export AGENT_HUMAN_STREAM_NAME="$NAME"
export AGENT_HUMAN_STREAM_REGISTRY="$REGISTRY"
export AGENT_HUMAN_STREAM_PID="$$"
# Worker marker inherited by the backend's tool shells (sume#7839): the
# launcher/wrapper refuse to start a nested worker when they see it.
export SUME_WORKER_SESSION="${NAME:-$BACKEND}"
export AGENT_HUMAN_STREAM_CWD="$PWD"
export AGENT_HUMAN_STREAM_RESUME_FROM="${RESUME:-}"
if [[ "$CONTINUE" -eq 1 ]]; then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def test_fork_launchers() -> None:
base = Path(tmp)
env = os.environ.copy()
for key in list(env):
if key.startswith(("AGENT_", "CLAUDE_", "GROK_")):
if key.startswith(("AGENT_", "CLAUDE_", "GROK_", "SUME_")):
env.pop(key)
env.update(PATH=f"{tmp}:{env['PATH']}", TOKENMAXXING_REQUIRE_SUPERVISOR="0",
AGENT_HUMAN_STREAM_REGISTRY=str(base / "registry.jsonl"),
Expand Down Expand Up @@ -228,7 +228,10 @@ def test_fork_launchers() -> None:
proc = subprocess.run(args, env=env, text=True, capture_output=True)
assert proc.returncode == 0, proc.stderr
argv = json.loads((base / "argv.json").read_text())
assert parent in argv and "Fork fixture prompt" in argv, argv
assert parent in argv, argv
# The wrapper prepends the worker preamble (sume#7839); the task text stays last.
assert any(a.startswith("[sume worker session]") and a.endswith("\n\nFork fixture prompt")
for a in argv), argv
if backend == "codex":
assert argv[:2] == ["exec", "fork"], argv
assert "resume" not in argv and "--resume" not in argv, argv
Expand Down Expand Up @@ -306,7 +309,7 @@ def test_codex_file_drain() -> None:
stub.chmod(0o755)
env = os.environ.copy()
for key in list(env):
if key.startswith(("AGENT_", "CLAUDE_", "GROK_")):
if key.startswith(("AGENT_", "CLAUDE_", "GROK_", "SUME_")):
env.pop(key)
env.update(PATH=f"{tmp}:{env['PATH']}", TOKENMAXXING_REQUIRE_SUPERVISOR="0",
AGENT_HUMAN_STREAM_REGISTRY=str(base / "registry.jsonl"),
Expand All @@ -326,9 +329,77 @@ def test_codex_file_drain() -> None:
assert len(list((base / "live").glob("*.codex.jsonl"))) == 2


def test_worker_preamble_and_nested_guard() -> None:
"""Every launch gets the worker preamble; a launch from inside a worker exits 5 (sume#7839)."""
with tempfile.TemporaryDirectory() as tmp:
base = Path(tmp)
env = os.environ.copy()
for key in list(env):
if key.startswith(("AGENT_", "CLAUDE_", "GROK_", "SUME_")):
env.pop(key)
env.update(PATH=f"{tmp}:{env['PATH']}", TOKENMAXXING_REQUIRE_SUPERVISOR="0",
AGENT_HUMAN_STREAM_REGISTRY=str(base / "registry.jsonl"),
AGENT_HUMAN_STREAM_LIVE_DIR=str(base / "live"),
ARGV_FILE=str(base / "argv.json"))
stub = base / "codex"
stub.write_text("""#!/usr/bin/env python3
import json, os, sys
from pathlib import Path
Path(os.environ['ARGV_FILE']).write_text(json.dumps(sys.argv[1:]))
Path(os.environ['ENV_FILE']).write_text(json.dumps({k: v for k, v in os.environ.items() if k.startswith(('SUME_', 'AGENT_HUMAN_STREAM_PID'))}))
print(json.dumps({'type': 'thread.started', 'thread_id': 'cccccccc-cccc-7ccc-8ccc-cccccccccccc'}))
""")
stub.chmod(0o755)
env["ENV_FILE"] = str(base / "env.json")
wrapper = str(ROOT / "agent-human-stream.sh")
prompt = base / "prompt.md"
prompt.write_text("# Astra : preamble-test (#7839)\nWork in English. `--host mini`. Fresh worker.")
args = [wrapper, "--backend", "codex", "--name", "preamble-test", "--prompt-file", str(prompt), "--effort", "low"]

proc = subprocess.run(args, env=env, text=True, capture_output=True)
assert proc.returncode == 0, proc.stderr
argv = json.loads((base / "argv.json").read_text())
body = argv[-1]
assert body.startswith('[sume worker session] You are the WORKER for job "preamble-test"'), body
assert "Never run sume-bg-launch" in body, body
assert body.endswith("\n\n" + prompt.read_text()), body
seen = json.loads((base / "env.json").read_text())
assert seen.get("SUME_WORKER_SESSION") == "preamble-test", seen
assert seen.get("AGENT_HUMAN_STREAM_PID"), seen
rows = [json.loads(l) for l in (base / "registry.jsonl").read_text().splitlines() if l.strip()]
assert rows and rows[0]["prompt_head"].startswith("# Astra : preamble-test (#7839)"), rows[0]

# Opt-out keeps the prompt byte-identical.
proc = subprocess.run(args, env={**env, "AGENT_HUMAN_STREAM_WORKER_PREAMBLE": "0"},
text=True, capture_output=True)
assert proc.returncode == 0, proc.stderr
assert json.loads((base / "argv.json").read_text())[-1] == prompt.read_text()

# Inside a worker session (marker inherited by its tool shells) the launch is refused.
(base / "argv.json").unlink()
for marker in ({"SUME_WORKER_SESSION": "outer-job"}, {"AGENT_HUMAN_STREAM_PID": "4242"}):
proc = subprocess.run(args, env={**env, **marker}, text=True, capture_output=True)
assert proc.returncode == 5, (proc.returncode, proc.stderr)
assert "refusing a nested worker launch" in proc.stderr, proc.stderr
assert not (base / "argv.json").exists(), "nested launch must not start the backend"
proc = subprocess.run(args, env={**env, "SUME_WORKER_SESSION": "outer-job",
"SUME_BG_REMOTE_JOB": "20260101T000000Z-outer"},
text=True, capture_output=True)
assert proc.returncode == 5 and "Mini job 20260101T000000Z-outer" in proc.stderr, proc.stderr
# --sessions is a read; the override launches on purpose.
proc = subprocess.run([wrapper, "--sessions"], env={**env, "SUME_WORKER_SESSION": "outer-job"},
text=True, capture_output=True)
assert proc.returncode == 0, proc.stderr
proc = subprocess.run(args, env={**env, "SUME_WORKER_SESSION": "outer-job", "SUME_BG_ALLOW_NESTED": "1"},
text=True, capture_output=True)
assert proc.returncode == 0 and "nested launch allowed" in proc.stderr, proc.stderr
assert (base / "argv.json").exists()


def main() -> None:
test_codex_file_drain()
test_fork_launchers()
test_worker_preamble_and_nested_guard()
claude = run_stream(CLAUDE_STREAM, "claude")
require(claude, "📎 session_id=11111111-1111-1111-1111-111111111111")
require(claude, "backend=claude")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
# Control plane: --attach <job> | --status <job> | --kill <job> | --jobs
# Docs: docs/MINI-WORKER-HOST.md
#
# Nested launches are refused (sume#7839): when this runs inside a worker
# session that agent-human-stream started (env SUME_WORKER_SESSION or
# AGENT_HUMAN_STREAM_PID), a new launch exits 5. A worker that spawns another
# worker is the ghost pattern — the child is killed when the parent's tool
# call/session ends (--host local) or duplicates the job on the Mini
# (--host mini from the Mini). Control-plane reads still work.
# Override on purpose only: SUME_BG_ALLOW_NESTED=1.
#
# Shell: description = "Grok : <job-slug> (#N)" (or "Codex : …"), block_until_ms = 0.
# After spawn: read the terminal once for 📎 session_id= or exit_code.
set -euo pipefail
Expand Down Expand Up @@ -300,6 +308,52 @@ mini_attach() {
exit "$rc"
}


# ---------------------------------------------------------------------------
# Nested-launch guard (sume#7839)
# ---------------------------------------------------------------------------

# Non-empty when this process runs inside a wrapper-started worker session.
# Only the wrapper's session markers count: `sume-bg-remote run` exports
# SUME_BG_REMOTE_JOB before it starts the wrapper, so that alone is not nested.
nested_worker_context() {
local ctx=""
if [[ -n "${SUME_WORKER_SESSION:-}" ]]; then
ctx="worker session ${SUME_WORKER_SESSION}"
elif [[ -n "${AGENT_HUMAN_STREAM_PID:-}" ]]; then
ctx="worker session ${AGENT_HUMAN_STREAM_NAME:-?} (wrapper pid ${AGENT_HUMAN_STREAM_PID})"
fi
if [[ -n "$ctx" && -n "${SUME_BG_REMOTE_JOB:-}" ]]; then
ctx="${ctx} = Mini job ${SUME_BG_REMOTE_JOB}"
fi
printf '%s' "$ctx"
}

refuse_nested_launch() {
local ctx
ctx=$(nested_worker_context)
[[ -n "$ctx" ]] || return 0
if [[ "${SUME_BG_ALLOW_NESTED:-0}" == "1" ]]; then
echo "note: nested launch allowed by SUME_BG_ALLOW_NESTED=1 (inside $ctx)" >&2
return 0
fi
cat >&2 <<EOF
error: refusing a nested worker launch — you are already inside $ctx.
You ARE the worker for this job. Do the task in this session; do not
spawn, resume, or hand off to another worker. A nested --host local
worker is killed when your tool call/session ends, and a nested
--host mini launch duplicates the job on the Mini (sume#7839).
Launcher flags quoted in your prompt (--backend/--model/--host mini,
"Fresh worker", "Job title") describe how THIS session was started.
Override only on purpose: SUME_BG_ALLOW_NESTED=1.
EOF
exit 5
}

if [[ "$ACTION" == "launch" ]]; then
refuse_nested_launch
fi

if [[ "$ACTION" != "launch" ]]; then
if [[ "$HOST" != "mini" ]]; then
echo "error: --$ACTION is a Mini control-plane action; add --host mini" >&2
Expand Down
Loading