#!/usr/bin/env bash
# Task24 real-Telegram, single-human-window controller.
#
# This script never calls Telegram or the model provider itself and never writes the
# live profile. The human alone presses Telegram buttons and sends the one edit text.
# After one explicit, state-bound approval it may restart the user gateway service at
# the two named checkpoints. All other checks are read-only snapshots written outside
# the profile under /home/cube/.cache/senpi-task23-tmp.
set -euo pipefail

readonly REPO=/home/cube/projects/richard/hermes-agent
readonly PROFILE=/home/cube/.hermes/profiles/dualcoachtest
readonly SERVICE=hermes-gateway-dualcoachtest.service
readonly TMP_ROOT=/home/cube/.cache/senpi-task23-tmp
readonly BASE_TOKEN=3f44a18ea620d963
readonly BASE_MESSAGE_ID=153
readonly EDIT_TEXT='현재 계획을 유지하며 다음 기록을 확인하겠습니다.'
readonly APPROVAL_PHRASE='TASK24_REAL_TELEGRAM_OWNER_APPROVED'
readonly OWNER_ACTIONS="$PROFILE/data/owner-actions"
readonly LIVE_CHECKPOINT='/home/cube/projects/richard/traning coach/.omo/evidence/task24-live-window-checkpoint.json'
CHECKPOINT_TOKEN="$BASE_TOKEN"
CHECKPOINT_MESSAGE_ID="$BASE_MESSAGE_ID"
WATCH_PID=''

usage() {
  cat <<'EOF'
Usage:
  dualcoach-task-24-real-one-window.sh --seal
  TASK24_LIVE_WINDOW_APPROVAL=TASK24_REAL_TELEGRAM_OWNER_APPROVED \
  TASK24_LIVE_WINDOW_SEAL="$(dualcoach-task-24-real-one-window.sh --seal)" \
  dualcoach-task-24-real-one-window.sh --preflight
  TASK24_LIVE_WINDOW_APPROVAL=TASK24_REAL_TELEGRAM_OWNER_APPROVED \
  TASK24_LIVE_WINDOW_SEAL="$(dualcoach-task-24-real-one-window.sh --seal)" \
  dualcoach-task-24-real-one-window.sh --run

--seal and --preflight are read-only with respect to the live profile and service.
--run is the only mode that can restart the user gateway, and it refuses unless the
one explicit approval phrase and the exact current-state seal are both supplied.
EOF
}

snapshot() {
  local target="$1"
  PROFILE_PATH="$PROFILE" TARGET="$target" CURRENT_BASE_TOKEN="$BASE_TOKEN" CURRENT_BASE_MESSAGE_ID="$BASE_MESSAGE_ID" \
    python3 - <<'PY'
import hashlib
import json
import os
from pathlib import Path
import yaml

profile = Path(os.environ['PROFILE_PATH'])
target = Path(os.environ['TARGET'])
base_token = os.environ['CURRENT_BASE_TOKEN']
base_message_id = os.environ['CURRENT_BASE_MESSAGE_ID']
owner = profile / 'data' / 'owner-actions'

def load(name, default):
    path = owner / name
    return json.loads(path.read_text(encoding='utf-8')) if path.exists() else default

config = yaml.safe_load((profile / 'config.yaml').read_text(encoding='utf-8'))
extra = (((config or {}).get('platforms') or {}).get('telegram') or {}).get('extra') or {}
adaptive = extra.get('adaptive_nutrition') if isinstance(extra, dict) else {}
registry = json.loads((profile / 'customers' / 'registry.json').read_text(encoding='utf-8'))
customers = registry.get('customers') if isinstance(registry, dict) else []
customer_enabled = bool(customers and isinstance(customers[0], dict) and customers[0].get('enabled') is True)
drafts = load('drafts.json', {})
generations = load('draft-generations.json', {})
cards = load('draft-generation-cards.json', {})
deliveries = load('draft-deliveries.json', {})

# Deliberately exclude draft text, raw model content, chat/user IDs, and provider IDs.
rows = {}
for token in sorted(drafts):
    draft = drafts[token]
    history = generations.get(token, [])
    generation = history[-1] if history else {}
    card = cards.get(token, {})
    rows[token] = {
        'draft_status': draft.get('status'),
        'parent_draft_id': draft.get('parent_draft_id'),
        'generation_state': generation.get('state'),
        'generation': generation.get('generation'),
        'generation_provider_receipt_present': isinstance(generation.get('generation_provider_receipt'), str),
        'delivery_provider_receipt_present': isinstance(generation.get('delivery_provider_receipt'), str),
        'card_message_id': card.get('message_id'),
        'card_state': card.get('state'),
        'card_owner_dm_topic_zero': (
            isinstance(card.get('destination'), dict)
            and str(card['destination'].get('topic_id')) == '0'
            and str(card['destination'].get('chat_id')) == str(card['destination'].get('user_id'))
        ),
    }
gateway_state = json.loads((profile / 'gateway_state.json').read_text(encoding='utf-8'))
platforms = gateway_state.get('platforms') if isinstance(gateway_state, dict) else {}
telegram = platforms.get('telegram') if isinstance(platforms, dict) else {}
state = {
    'schema': 'task24-live-window-observer-v1',
    'config_delivery_enabled': isinstance(adaptive, dict) and adaptive.get('delivery_enabled') is True,
    'customer_enabled': customer_enabled,
    'gateway_state': gateway_state.get('gateway_state') if isinstance(gateway_state, dict) else None,
    'telegram_connection_state': telegram.get('state') if isinstance(telegram, dict) else None,
    'gateway_state_file_sha256': hashlib.sha256((profile / 'gateway_state.json').read_bytes()).hexdigest(),
    'drafts': rows,
    'delivery_count': len(deliveries),
    'delivery_statuses': sorted(str(value.get('status')) for value in deliveries.values() if isinstance(value, dict)),
    'baseline': rows.get(base_token),
    'baseline_message_id': base_message_id,
}
target.write_text(json.dumps(state, sort_keys=True, indent=2), encoding='utf-8')
target.chmod(0o600)
print(hashlib.sha256(target.read_bytes()).hexdigest())
PY
}

seal() {
  local temp
  mkdir -p "$TMP_ROOT"
  temp="$(mktemp "${TMP_ROOT}/task24-seal.XXXXXX")"
  trap 'rm -f "$temp"' RETURN
  snapshot "$temp" >/dev/null
  python3 - "$temp" <<'PY'
import hashlib
import json
import sys
state = json.loads(open(sys.argv[1], encoding='utf-8').read())
bound = {
    'schema': 'task24-real-telegram-window-v1',
    'base_token': '3f44a18ea620d963',
    'base_message_id': '153',
    'config_delivery_enabled': state['config_delivery_enabled'],
    'customer_enabled': state['customer_enabled'],
    'delivery_count': state['delivery_count'],
    'baseline': state['baseline'],
}
print(hashlib.sha256(json.dumps(bound, sort_keys=True, separators=(',', ':')).encode()).hexdigest())
PY
  trap - RETURN
}

assert_stage() {
  local stage="$1"
  local before="$2"
  local after="$3"
  local selected_file="$4"
  python3 - "$stage" "$before" "$after" "$selected_file" <<'PY'
import json
import sys
from pathlib import Path

stage, before_path, after_path, selected_path = sys.argv[1:]
before = json.loads(Path(before_path).read_text(encoding='utf-8'))
after = json.loads(Path(after_path).read_text(encoding='utf-8'))
base = '3f44a18ea620d963'

def fail(message):
    raise SystemExit(f'BLOCK[{stage}]: {message}')

def row(state, token):
    value = state['drafts'].get(token)
    if not isinstance(value, dict):
        fail(f'missing draft projection for {token}')
    return value

def one_new(previous, current, parent, status):
    tokens = sorted(set(current['drafts']) - set(previous['drafts']))
    if len(tokens) != 1:
        fail(f'expected exactly one new immutable child, found {len(tokens)}')
    token = tokens[0]
    value = row(current, token)
    if value.get('parent_draft_id') != parent:
        fail('new child does not bind to the expected parent')
    if value.get('draft_status') != status:
        fail(f'new child is {value.get("draft_status")!r}, expected {status!r}')
    if value.get('generation_state') != 'draft_created':
        fail('new child did not reach draft_created')
    if value.get('card_state') != 'published' or not value.get('card_message_id'):
        fail('new child has no published owner review card')
    if not value.get('card_owner_dm_topic_zero'):
        fail('new child card is not bound to the owner DM topic 0')
    return token, value

if stage == 'baseline':
    value = row(after, base)
    if after['config_delivery_enabled'] is not True:
        fail('adaptive_nutrition.delivery_enabled is false; an explicitly approved configuration deployment is required before this window')
    if after['customer_enabled'] is not True:
        fail('synthetic customer is not enabled')
    if after['gateway_state'] != 'running' or after['telegram_connection_state'] != 'connected':
        fail('gateway state file does not report running Telegram connected')
    if after['delivery_count'] != 0:
        fail('delivery outbox is not empty')
    expected = {
        'draft_status': 'created',
        'generation_state': 'draft_created',
        'generation': 3,
        'generation_provider_receipt_present': True,
        'delivery_provider_receipt_present': False,
        'card_message_id': '153',
        'card_state': 'published',
        'card_owner_dm_topic_zero': True,
    }
    for key, wanted in expected.items():
        if value.get(key) != wanted:
            fail(f'baseline {key} is {value.get(key)!r}, expected {wanted!r}')
    Path(selected_path).write_text('BASE_TOKEN=3f44a18ea620d963\nBASE_MESSAGE_ID=153\n', encoding='utf-8')
elif stage == 'regenerated':
    token, value = one_new(before, after, base, 'created')
    if not value.get('generation_provider_receipt_present'):
        fail('regeneration lacks a provider generation receipt')
    Path(selected_path).write_text(f'REGEN_TOKEN={token}\nREGEN_MESSAGE_ID={value["card_message_id"]}\n', encoding='utf-8')
elif stage == 'edited':
    prior = Path(selected_path).read_text(encoding='utf-8').splitlines()
    regen = dict(line.split('=', 1) for line in prior)['REGEN_TOKEN']
    token, value = one_new(before, after, regen, 'edited')
    Path(selected_path).write_text('\n'.join(prior) + f'\nEDIT_TOKEN={token}\nEDIT_MESSAGE_ID={value["card_message_id"]}\n', encoding='utf-8')
elif stage in {'approved', 'restart_before_send'}:
    values = dict(line.split('=', 1) for line in Path(selected_path).read_text(encoding='utf-8').splitlines())
    value = row(after, values['EDIT_TOKEN'])
    if after['delivery_count'] != 0:
        fail('a delivery exists before explicit send')
    if value.get('draft_status') != 'approved' or value.get('generation_state') != 'approved':
        fail('edited child is not durably approved')
elif stage in {'sent', 'restart_after_send'}:
    values = dict(line.split('=', 1) for line in Path(selected_path).read_text(encoding='utf-8').splitlines())
    value = row(after, values['EDIT_TOKEN'])
    if after['delivery_count'] != 1 or after['delivery_statuses'] != ['sent_audited']:
        fail('expected exactly one sent_audited delivery receipt')
    if value.get('draft_status') != 'sent' or value.get('generation_state') != 'sent_audited':
        fail('edited child did not reach sent_audited')
    if not value.get('delivery_provider_receipt_present'):
        fail('sent_audited generation lacks its provider receipt')
else:
    fail('unknown observer stage')
print(f'OBSERVER[{stage}]: PASS')
PY
}

write_checkpoint() {
  local state="$1"
  STATE="$state" MESSAGE_ID="$CHECKPOINT_MESSAGE_ID" TOKEN="$CHECKPOINT_TOKEN" \
    OWNER_ACTIONS_PATH="$OWNER_ACTIONS" CHECKPOINT_PATH="$LIVE_CHECKPOINT" python3 - <<'PY'
import ctypes
import datetime as dt
import json
import os
import tempfile
from pathlib import Path

owner = Path(os.environ['OWNER_ACTIONS_PATH'])
checkpoint = Path(os.environ['CHECKPOINT_PATH'])
generations_path = owner / 'draft-generations.json'
deliveries_path = owner / 'draft-deliveries.json'
generations = json.loads(generations_path.read_text(encoding='utf-8'))
deliveries = json.loads(deliveries_path.read_text(encoding='utf-8')) if deliveries_path.exists() else {}
receipts = {
    record['generation_provider_receipt']
    for history in generations.values() if isinstance(history, list)
    for record in history if isinstance(record, dict)
    and isinstance(record.get('generation_provider_receipt'), str)
}
payload = {
    'state': os.environ['STATE'],
    'current_message_id': os.environ['MESSAGE_ID'],
    'current_token': os.environ['TOKEN'],
    'delivery_count': len(deliveries),
    'provider_count': len(receipts),
    'timestamp': dt.datetime.now(dt.timezone.utc).isoformat().replace('+00:00', 'Z'),
}
fd, temporary = tempfile.mkstemp(prefix='.task24-live-window-checkpoint.', dir=str(checkpoint.parent))
try:
    os.fchmod(fd, 0o600)
    with os.fdopen(fd, 'w', encoding='utf-8') as handle:
        json.dump(payload, handle, sort_keys=True, separators=(',', ':'))
        handle.write('\n')
        handle.flush()
        os.fsync(handle.fileno())
    os.replace(temporary, checkpoint)
    directory = os.open(checkpoint.parent, os.O_DIRECTORY)
    try:
        os.fsync(directory)
    finally:
        os.close(directory)
except BaseException:
    try:
        os.unlink(temporary)
    except FileNotFoundError:
        pass
    raise
PY
}

arm_transition() {
  local expected_stage="$1" event_file="$2" ready_fifo armed
  if [[ -n "$WATCH_PID" ]]; then
    echo 'BLOCK: an owner-action observer is already armed.' >&2
    write_checkpoint BLOCKED_OBSERVER
    exit 1
  fi
  ready_fifo="${event_file}.armed"
  mkfifo -m 600 "$ready_fifo"
  OWNER_ACTIONS_PATH="$OWNER_ACTIONS" EVENT_FILE="$event_file" READY_FIFO="$ready_fifo" EXPECTED_STAGE="$expected_stage" python3 - <<'PY' &
import ctypes
import datetime as dt
import json
import os
import select
from pathlib import Path

IN_CLOSE_WRITE = 0x00000008
IN_MOVED_TO = 0x00000080
IN_ONLYDIR = 0x01000000
libc = ctypes.CDLL(None, use_errno=True)
fd = libc.inotify_init1(os.O_CLOEXEC)
if fd < 0:
    raise OSError(ctypes.get_errno(), 'inotify_init1')
try:
    watch = libc.inotify_add_watch(fd, os.fsencode(os.environ['OWNER_ACTIONS_PATH']), IN_CLOSE_WRITE | IN_MOVED_TO | IN_ONLYDIR)
    if watch < 0:
        raise OSError(ctypes.get_errno(), 'inotify_add_watch')
    with open(os.environ['READY_FIFO'], 'w', encoding='ascii') as ready_file:
        ready_file.write('ARMED\n')
        ready_file.flush()
    ready, _, _ = select.select([fd], [], [], 1800)
    if not ready:
        raise SystemExit('BLOCK: durable transition observer timed out')
    os.read(fd, 4096)
    Path(os.environ['EVENT_FILE']).write_text(json.dumps({
        'stage': os.environ['EXPECTED_STAGE'],
        'observed_at': dt.datetime.now(dt.timezone.utc).isoformat().replace('+00:00', 'Z'),
    }, sort_keys=True) + '\n', encoding='utf-8')
finally:
    os.close(fd)
PY
  WATCH_PID="$!"
  if ! armed="$(timeout 15 cat "$ready_fifo")" || [[ "$armed" != 'ARMED' ]]; then
    rm -f "$ready_fifo"
    kill "$WATCH_PID" 2>/dev/null || true
    WATCH_PID=''
    echo 'BLOCK: could not arm the durable owner-action observer.' >&2
    write_checkpoint BLOCKED_OBSERVER
    exit 1
  fi
  rm -f "$ready_fifo"
}

await_transition() {
  local expected_stage="$1"
  if ! wait "$WATCH_PID"; then
    WATCH_PID=''
    echo "BLOCK: no durable owner-action transition for $expected_stage." >&2
    write_checkpoint BLOCKED_OBSERVER
    exit 1
  fi
  WATCH_PID=''
}

assert_stage_or_block() {
  local stage="$1" before="$2" after="$3" selected="$4"
  if ! assert_stage "$stage" "$before" "$after" "$selected"; then
    write_checkpoint BLOCKED_OBSERVER
    exit 1
  fi
}

set_checkpoint_identity_from_selected() {
  local token_key="$1" message_key="$2" selected="$3"
  CHECKPOINT_TOKEN="$(awk -F= -v key="$token_key" '$1 == key { print $2 }' "$selected")"
  CHECKPOINT_MESSAGE_ID="$(awk -F= -v key="$message_key" '$1 == key { print $2 }' "$selected")"
  if [[ -z "$CHECKPOINT_TOKEN" || -z "$CHECKPOINT_MESSAGE_ID" ]]; then
    echo 'BLOCK: durable observer did not publish the expected card identity.' >&2
    write_checkpoint BLOCKED_OBSERVER
    exit 1
  fi
}

restart_gateway() {
  systemctl --user restart "$SERVICE"
  systemctl --user is-active --quiet "$SERVICE" || {
    echo 'BLOCK: gateway did not return active after controlled restart.' >&2
    write_checkpoint BLOCKED_RESTART
    exit 1
  }
}

checkpoint() {
  local text="$1"
  printf '\n%s\n' "$text"
  read -r -p 'Press Enter only after the named human Telegram action and observation are complete. ' _
}

preflight_window() {
  local expected run selected
  expected="$(seal)"
  if [[ "${TASK24_LIVE_WINDOW_APPROVAL:-}" != "$APPROVAL_PHRASE" ]]; then
    echo 'BLOCK: TASK24_LIVE_WINDOW_APPROVAL does not contain the exact owner approval phrase.' >&2
    exit 64
  fi
  if [[ "${TASK24_LIVE_WINDOW_SEAL:-}" != "$expected" ]]; then
    echo 'BLOCK: TASK24_LIVE_WINDOW_SEAL does not bind this exact current card/delivery state.' >&2
    exit 64
  fi
  umask 077
  mkdir -p "$TMP_ROOT"
  run="$(mktemp -d "${TMP_ROOT}/task24-real-preflight.XXXXXX")"
  selected="$run/selected.env"
  snapshot "$run/baseline.json" >/dev/null
  assert_stage baseline "$run/baseline.json" "$run/baseline.json" "$selected"
  systemctl --user is-active --quiet "$SERVICE" || { echo 'BLOCK: gateway service is not active.' >&2; exit 1; }
  (
    cd "$REPO"
    HERMES_HOME="$PROFILE" .venv/bin/python -c \
      'from gateway.platforms.nutrition_coaching import preflight_nutrition_generation_provider as f; raise SystemExit(0 if f() else 1)'
  ) || { echo 'BLOCK: configured auxiliary-provider credential preflight failed.' >&2; exit 1; }
  (
    cd "$REPO"
    TMPDIR="$TMP_ROOT" TMP="$TMP_ROOT" TEMP="$TMP_ROOT" PYTHONDONTWRITEBYTECODE=1 \
      .venv/bin/python -m pytest -q -m integration \
      tests/gateway/test_task24_preflight_integration.py \
      --basetemp="$run/automated-matrix"
  ) | tee "$run/automated-matrix.log"
  cat <<EOF
READY_FOR_TASK24_ONE_WINDOW
transcript=$run
current_owner_card_message=153
current_owner_card_token=3f44a18ea620d963
current_controls=수정,재생성,승인,보류
first_human_action=Press 재생성 exactly once on message 153.
EOF
}

run_window() {
  local expected run baseline regenerated edited approved before_send sent after_send selected
  expected="$(seal)"
  if [[ "${TASK24_LIVE_WINDOW_APPROVAL:-}" != "$APPROVAL_PHRASE" ]]; then
    echo 'BLOCK: TASK24_LIVE_WINDOW_APPROVAL does not contain the exact owner approval phrase.' >&2
    exit 64
  fi
  if [[ "${TASK24_LIVE_WINDOW_SEAL:-}" != "$expected" ]]; then
    echo 'BLOCK: TASK24_LIVE_WINDOW_SEAL does not bind this exact current card/delivery state.' >&2
    exit 64
  fi

  umask 077
  mkdir -p "$TMP_ROOT"
  run="$(mktemp -d "${TMP_ROOT}/task24-real-window.XXXXXX")"
  selected="$run/selected.env"
  echo "Task24 observer transcript: $run"

  snapshot "$run/baseline.json" >/dev/null
  assert_stage baseline "$run/baseline.json" "$run/baseline.json" "$selected"
  systemctl --user is-active --quiet "$SERVICE" || { echo 'BLOCK: gateway service is not active.' >&2; exit 1; }
  # This resolves the configured local credential/client only; it does not send a
  # provider request. The later human Regenerate is the one real provider operation.
  (
    cd "$REPO"
    HERMES_HOME="$PROFILE" .venv/bin/python -c \
      'from gateway.platforms.nutrition_coaching import preflight_nutrition_generation_provider as f; raise SystemExit(0 if f() else 1)'
  ) || { echo 'BLOCK: configured auxiliary-provider credential preflight failed.' >&2; exit 1; }

  # This is the delegated stale/repeat/wrong-role matrix. It is a bwrap-isolated,
  # HTTP-boundary-mocked test and must pass before the human mutates Telegram.
  (
    cd "$REPO"
    TMPDIR="$TMP_ROOT" TMP="$TMP_ROOT" TEMP="$TMP_ROOT" PYTHONDONTWRITEBYTECODE=1 \
      .venv/bin/python -m pytest -q -m integration \
      tests/gateway/test_task24_preflight_integration.py \
      --basetemp="$run/automated-matrix"
  ) | tee "$run/automated-matrix.log"

  cat <<'EOF'

HUMAN TELEGRAM SEQUENCE (the script does not invoke these actions):
1. In the Owner private DM, use current review message 153 only. It is the
   published draft-created card for token 3f44a18ea620d963. Its current controls are:
   수정 | 재생성 / 승인 | 보류. Diagnostic-only current callbacks (never paste or replay):
   수정=n3:3f44a18ea620d963:e:f99c5ae3:99
   재생성=n3:3f44a18ea620d963:r:f99c5ae3:99
   승인=n3:3f44a18ea620d963:a:f99c5ae3:99
   보류=n3:3f44a18ea620d963:h:f99c5ae3:99
2. Press 재생성 once. Do not press any other control on message 153 afterward.
3. Wait for the newly published Owner-DM review card (new message ID is discovered by
   the observer; do not guess it). On that newest card press 수정 once.
4. Send exactly this one-line edit as a normal Owner-DM message:
   현재 계획을 유지하며 다음 기록을 확인하겠습니다.
5. Wait for the newly published immutable edited-child review card. On that newest
   card press 승인 once. Confirm its controls become 고객에게 보내기 | 승인 철회.
6. The script performs the controlled restart-before-send. After it returns active,
   confirm the same approved card remains visible; do not send yet.
7. Press 고객에게 보내기 once on that approved card. Confirm exactly one new coaching
   message appears in the dedicated synthetic customer DM. Do not press the button again.
8. The script performs the controlled restart-after-send. Do not interact with old,
   stale, or superseded cards at any point.

STOP immediately (do not retry, edit config, or click another card) if the newest card
shows generation failure, if any unexpected delivery appears before Send, if more than
one new current card appears for a stage, if the customer DM is not exactly one new
message, or if an observer prints BLOCK.
EOF

  # Subscribe before exposing each human action. The native inotify watcher observes
  # an owner-action directory transition; the following durable snapshot proves the
  # exact expected lifecycle transition and rejects unrelated writes.
  arm_transition regenerated "$run/regenerated.event.json"
  write_checkpoint WAITING_OWNER_REGENERATE_153
  echo 'WAITING_OWNER_REGENERATE_153'
  checkpoint 'Step 2 complete: regenerated card is visible in the Owner DM.'
  await_transition regenerated
  snapshot "$run/regenerated.json" >/dev/null
  assert_stage_or_block regenerated "$run/baseline.json" "$run/regenerated.json" "$selected"
  set_checkpoint_identity_from_selected REGEN_TOKEN REGEN_MESSAGE_ID "$selected"
  write_checkpoint OBSERVED_REGENERATE

  arm_transition edited "$run/edited.event.json"
  write_checkpoint WAITING_OWNER_EDIT_TEXT
  checkpoint 'Steps 3-4 complete: the exact edit text was sent and an edited-child card is visible.'
  await_transition edited
  snapshot "$run/edited.json" >/dev/null
  assert_stage_or_block edited "$run/regenerated.json" "$run/edited.json" "$selected"
  set_checkpoint_identity_from_selected EDIT_TOKEN EDIT_MESSAGE_ID "$selected"
  write_checkpoint OBSERVED_EDIT

  arm_transition approved "$run/approved.event.json"
  write_checkpoint WAITING_OWNER_APPROVE
  checkpoint 'Step 5 complete: the edited-child card shows 고객에게 보내기 | 승인 철회.'
  await_transition approved
  snapshot "$run/approved.json" >/dev/null
  assert_stage_or_block approved "$run/edited.json" "$run/approved.json" "$selected"
  write_checkpoint OBSERVED_APPROVE

  write_checkpoint RESTARTING_BEFORE_SEND
  restart_gateway
  snapshot "$run/before-send-restart.json" >/dev/null
  assert_stage_or_block restart_before_send "$run/approved.json" "$run/before-send-restart.json" "$selected"

  arm_transition sent "$run/sent.event.json"
  write_checkpoint WAITING_OWNER_SEND
  checkpoint 'Step 6 complete: restart returned active and the approved card remains visible; no Send was pressed. Then complete Step 7: Send once and observe exactly one synthetic customer-DM coaching message.'
  await_transition sent
  snapshot "$run/sent.json" >/dev/null
  assert_stage_or_block sent "$run/before-send-restart.json" "$run/sent.json" "$selected"
  write_checkpoint OBSERVED_SEND

  write_checkpoint RESTARTING_AFTER_SEND
  restart_gateway
  snapshot "$run/after-send-restart.json" >/dev/null
  assert_stage_or_block restart_after_send "$run/sent.json" "$run/after-send-restart.json" "$selected"

  write_checkpoint COMPLETE
  echo "READY_TO_SEAL_TASK24: all real-window observers passed; transcript=$run"
}

case "${1:-}" in
  --seal) seal ;;
  --preflight) preflight_window ;;
  --run) run_window ;;
  *) usage; exit 64 ;;
esac
