Skill: abeperl/codex-bridge (version 1.0.0)

--- name: codex-bridge description: Dispatch coding tasks to the local OpenAI Codex CLI with background execution, status polling, and answerable clarifying questions. Use when OpenClaw should delegate script building, code edits, refactors, or multi-step coding work to Codex from the command line. metadata: openclaw: emoji: "🧰" requires: bins: ["python3", "codex"] --- # Codex Bridge Delegate coding tasks from OpenClaw to the local `codex` CLI and manage them asynchronously. This skill provides a file-based bridge so OpenClaw can: - dispatch tasks - poll status and recent output - relay clarifying questions - send answers - collect final results ## When to Use - Build scripts (Python, Bash, etc.) - Implement or refactor code in an existing project - Run larger multi-file coding tasks in the background - Delegate coding work while keeping OpenClaw responsive - Handle tasks that may require clarifying questions mid-run ## When NOT to Use - Quick factual questions or explanations - Small code snippets that OpenClaw can write directly - Non-coding tasks - Tasks that should not invoke a local coding agent/CLI ## Dispatch a Task ```bash ~/.openclaw/skills/codex-bridge/codex-bridge-dispatch.sh \ --task-id <descriptive-name> \ --workdir <project-directory> \ --prompt "<detailed coding task>" ``` ### Prompt Writing Include: - what to build/fix - file paths if known - expected behavior/output - language/framework preferences - constraints (tests, style, no new deps, etc.) Example: ```bash ~/.openclaw/skills/codex-bridge/codex-bridge-dispatch.sh \ --task-id scripts-csv-parser \ --workdir ~/projects/data-tools \ --prompt "Create parse_orders.py. Read orders CSV, keep shipped rows, group by customer_id, and write summary CSV with columns customer_id, order_count, total_amount. Use pandas. Add basic CLI args and error handling." ``` ## Check Status ```bash ~/.openclaw/skills/codex-bridge/codex-bridge-status.sh --task-id <id> ``` Common status commands: ```bash ~/.openclaw/skills/codex-bridge/codex-bridge-status.sh --list ~/.openclaw/skills/codex-bridge/codex-bridge-status.sh --task-id <id> --output ~/.openclaw/skills/codex-bridge/codex-bridge-status.sh --task-id <id> --question ~/.openclaw/skills/codex-bridge/codex-bridge-status.sh --task-id <id> --result ~/.openclaw/skills/codex-bridge/codex-bridge-status.sh --task-id <id> --log ``` ## Answer Clarifying Questions When status is `waiting_for_answer`, read the pending question and send a response: ```bash ~/.openclaw/skills/codex-bridge/codex-bridge-status.sh --task-id <id> --question ~/.openclaw/skills/codex-bridge/codex-bridge-answer.sh --task-id <id> --answer "<answer text>" ``` The bridge resumes the same Codex session after the answer is written. ## Workflow 1. Dispatch task with a clear prompt. 2. Report the task ID. 3. Poll status/output periodically. 4. If status becomes `waiting_for_answer`, read `--question`, relay to user, then write answer with `--answer`. 5. When status is `complete`, read `--result` and summarize outcomes. 6. If status is `error`, inspect `--log` and `--output`. ## Notes and Limits - Uses the local `codex` CLI (`codex exec` and `codex exec resume`). - Clarifying questions are implemented via a lightweight text marker protocol in the prompt wrapper. - Bridge state is stored in `~/.codex-bridge/tasks/<task-id>/`. - Commands run via Codex in the specified `--workdir`. - For unattended runs, the bridge times out after 10 minutes waiting for an answer and resumes with a default/sensible fallback.

path: bridge.py
content: #!/usr/bin/env python3
"""
bridge.py - File-based task bridge between OpenClaw and OpenAI Codex CLI.

The bridge runs `codex exec --json` in the background, stores status/results on disk,
and supports basic two-way clarification by asking Codex to emit a structured question
marker that OpenClaw can relay to the user. Answers are written to `answer.json`, then
the bridge resumes the same Codex session with `codex exec resume`.
"""

from __future__ import annotations

import argparse
import json
import os
import re
import signal
import subprocess
import sys
import threading
import time
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any

BRIDGE_HOME = Path.home() / ".codex-bridge"
TASKS_DIR = BRIDGE_HOME / "tasks"
POLL_INTERVAL = 2.0
ANSWER_TIMEOUT = 600  # 10 minutes

QUESTION_RE = re.compile(
    r"\[\[OPENCLAW_QUESTION\]\]\s*(.*?)\s*\[\[/OPENCLAW_QUESTION\]\]",
    re.DOTALL,
)
OPTIONS_RE = re.compile(
    r"\[\[OPENCLAW_OPTIONS\]\]\s*(.*?)\s*\[\[/OPENCLAW_OPTIONS\]\]",
    re.DOTALL,
)


def now_iso() -> str:
    return datetime.now().isoformat()


def write_json_atomic(path: Path, data: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(json.dumps(data, indent=2, default=str))
    tmp.rename(path)


def read_json(path: Path) -> dict[str, Any] | None:
    if not path.exists():
        return None
    return json.loads(path.read_text())


def append_text(path: Path, text: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("a", encoding="utf-8") as f:
        f.write(text)


def write_status(task_dir: Path, status: str, detail: str = "") -> None:
    payload = {
        "status": status,
        "detail": detail,
        "updated_at": now_iso(),
        "pid": os.getpid(),
    }
    # Preserve thread id and round in status for easier debugging.
    task = read_json(task_dir / "task.json") or {}
    if "thread_id" in task:
        payload["thread_id"] = task["thread_id"]
    if "round" in task:
        payload["round"] = task["round"]
    write_json_atomic(task_dir / "status.json", payload)


def update_task(task_dir: Path, **updates: Any) -> dict[str, Any]:
    task = read_json(task_dir / "task.json") or {}
    task.update(updates)
    write_json_atomic(task_dir / "task.json", task)
    return task


def build_initial_prompt(user_prompt: str) -> str:
    protocol = """
You are operating as a coding sub-agent behind an OpenClaw bridge.

Follow this clarification protocol exactly when blocked by missing requirements:
1. If you need user input before proceeding safely, STOP and reply using only:
[[OPENCLAW_QUESTION]]
<single concise question>
[[/OPENCLAW_QUESTION]]
2. If useful, add options using only:
[[OPENCLAW_OPTIONS]]
<option 1>
<option 2>
...
[[/OPENCLAW_OPTIONS]]
3. Do not include any other text when asking a clarification question.
4. After receiving an answer, continue work immediately.

When no clarification is needed, perform the task normally and provide your usual final response.
""".strip()
    return f"{protocol}\n\nUser task:\n{user_prompt}"


def build_resume_prompt(answer_text: str) -> str:
    return (
        "Bridge answer from OpenClaw user:\n"
        f"{answer_text}\n\n"
        "Continue the task. If more clarification is needed, use the exact OPENCLAW_QUESTION/OPTIONS markers."
    )


def extract_question(text: str) -> dict[str, Any] | None:
    q_match = QUESTION_RE.search(text)
    if not q_match:
        return None
    question = q_match.group(1).strip()
    options: list[dict[str, str]] = []
    opt_match = OPTIONS_RE.search(text)
    if opt_match:
        for line in opt_match.group(1).splitlines():
            line = line.strip().lstrip("-").strip()
            if line:
                options.append({"label": line})
    return {
        "questions": [{"question": question, "options": options}],
        "asked_at": now_iso(),
    }


def collect_strings(value: Any, *, keys: set[str] | None = None) -> list[str]:
    out: list[str] = []
    if isinstance(value, dict):
        for k, v in value.items():
            if keys is None or k in keys:
                if isinstance(v, str):
                    out.append(v)
                else:
                    out.extend(collect_strings(v, keys=keys))
            else:
                out.extend(collect_strings(v, keys=keys))
    elif isinstance(value, list):
        for item in value:
            out.extend(collect_strings(item, keys=keys))
    return out


@dataclass
class RunCapture:
    thread_id: str | None = None
    last_assistant_text: str = ""
    assistant_fragments: list[str] = field(default_factory=list)
    errors: list[str] = field(default_factory=list)
    saw_json_events: int = 0


class CodexRunner:
    def __init__(self, task_dir: Path):
        self.task_dir = task_dir
        self.events_file = task_dir / "events.jsonl"
        self.output_file = task_dir / "output.log"
        self.bridge_log = task_dir / "bridge.log"

    def _read_stdout(self, proc: subprocess.Popen[str], capture: RunCapture) -> None:
        assert proc.stdout is not None
        for raw in proc.stdout:
            line = raw.rstrip("\n")
            append_text(self.bridge_log, f"[stdout] {line}\n")
            try:
                event = json.loads(line)
            except json.JSONDecodeError:
                # Some builds may emit non-JSON lines even with --json.
                append_text(self.output_file, raw)
                continue

            capture.saw_json_events += 1
            append_text(self.events_file, json.dumps(event) + "\n")
            self._handle_event(event, capture)

    def _read_stderr(self, proc: subprocess.Popen[str]) -> None:
        assert proc.stderr is not None
        for raw in proc.stderr:
            append_text(self.bridge_log, f"[stderr] {raw}")

    def _handle_event(self, event: dict[str, Any], capture: RunCapture) -> None:
        event_type = str(event.get("type", ""))
        if event_type == "thread.started":
            thread_id = event.get("thread_id")
            if isinstance(thread_id, str):
                capture.thread_id = thread_id
                append_text(self.output_file, f"[thread] {thread_id}\n")
            return

        if event_type == "turn.started":
            append_text(self.output_file, "[turn] started\n")
            return

        if event_type == "turn.completed":
            append_text(self.output_file, "[turn] completed\n")
            return

        if event_type == "error":
            msg = str(event.get("message", "Unknown Codex error"))
            capture.errors.append(msg)
            append_text(self.output_file, f"[error] {msg}\n")
            return

        # Try to harvest textual assistant output from common JSONL event shapes.
        text_fragments: list[str] = []
        if any(token in event_type for token in ("output_text", "assistant", "message", "response")):
            text_fragments = collect_strings(event, keys={"text", "delta", "message", "output_text"})
        if text_fragments:
            merged = "".join(text_fragments).strip()
            if merged:
                # Keep a stitched copy so marker-based questions can survive delta splitting.
                if not capture.assistant_fragments or capture.assistant_fragments[-1] != merged:
                    capture.assistant_fragments.append(merged)
                capture.last_assistant_text = "".join(capture.assistant_fragments)
                # Avoid very noisy duplicate delta spam.
                if not merged.startswith("Reconnecting..."):
                    append_text(self.output_file, merged + "\n")

        # Surface tool/command summaries if present.
        command_texts = collect_strings(event, keys={"command", "cmd"})
        for cmd in command_texts:
            if cmd and len(cmd) < 2000:
                append_text(self.output_file, f"[command] {cmd}\n")

    def run(self, cmd: list[str]) -> tuple[int, RunCapture]:
        capture = RunCapture()
        append_text(self.bridge_log, f"\n[{now_iso()}] RUN {' '.join(cmd)}\n")

        proc = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            stdin=subprocess.DEVNULL,
            text=True,
            bufsize=1,
        )

        t_out = threading.Thread(target=self._read_stdout, args=(proc, capture), daemon=True)
        t_err = threading.Thread(target=self._read_stderr, args=(proc,), daemon=True)
        t_out.start()
        t_err.start()
        returncode = proc.wait()
        t_out.join(timeout=2)
        t_err.join(timeout=2)

        append_text(self.bridge_log, f"[{now_iso()}] EXIT {returncode}\n")
        return returncode, capture


def codex_base_cmd() -> list[str]:
    return ["codex"]


def build_exec_cmd(workdir: str, prompt: str) -> list[str]:
    return (
        codex_base_cmd()
        + [
            "exec",
            "--json",
            "--color",
            "never",
            "--full-auto",
            "--skip-git-repo-check",
            "-C",
            workdir,
            prompt,
        ]
    )


def build_resume_cmd(thread_id: str, answer_prompt: str) -> list[str]:
    return (
        codex_base_cmd()
        + [
            "exec",
            "resume",
            "--json",
            "--full-auto",
            "--skip-git-repo-check",
            thread_id,
            answer_prompt,
        ]
    )


def wait_for_answer(task_dir: Path, question_payload: dict[str, Any]) -> str:
    write_json_atomic(task_dir / "question.json", question_payload)
    write_status(task_dir, "waiting_for_answer", "Codex is asking a clarifying question")
    (task_dir / "answer.json").unlink(missing_ok=True)

    start = time.time()
    while True:
        answer_file = task_dir / "answer.json"
        if answer_file.exists():
            answer_data = read_json(answer_file) or {}
            text = str(answer_data.get("text", "")).strip()
            (task_dir / "question.json").unlink(missing_ok=True)
            answer_file.unlink(missing_ok=True)
            if text:
                write_status(task_dir, "running", "Received answer, resuming Codex")
                return text

        if time.time() - start > ANSWER_TIMEOUT:
            (task_dir / "question.json").unlink(missing_ok=True)
            q = question_payload["questions"][0]
            opts = q.get("options") or []
            fallback = opts[0]["label"] if opts else "Use a sensible default and continue."
            write_status(task_dir, "running", "Answer timed out, continuing with default")
            return fallback

        time.sleep(POLL_INTERVAL)


def write_result(task_dir: Path, status: str, detail: str, capture: RunCapture, returncode: int) -> None:
    result = {
        "status": status,
        "detail": detail,
        "completed_at": now_iso(),
        "returncode": returncode,
        "thread_id": capture.thread_id,
        "last_message": capture.last_assistant_text,
        "errors": capture.errors,
        "json_event_count": capture.saw_json_events,
    }
    write_json_atomic(task_dir / "result.json", result)


def run_bridge(task_id: str, workdir: str, prompt: str) -> None:
    task_dir = TASKS_DIR / task_id
    task_dir.mkdir(parents=True, exist_ok=True)
    (task_dir / "bridge.pid").write_text(str(os.getpid()))

    write_json_atomic(
        task_dir / "task.json",
        {
            "task_id": task_id,
            "workdir": workdir,
            "prompt": prompt,
            "created_at": now_iso(),
            "round": 0,
        },
    )
    (task_dir / "output.log").touch()
    (task_dir / "events.jsonl").touch()
    (task_dir / "bridge.log").touch()

    runner = CodexRunner(task_dir)
    write_status(task_dir, "starting", "Initializing Codex task")

    current_cmd = build_exec_cmd(workdir, build_initial_prompt(prompt))
    thread_id: str | None = None
    rounds = 0

    while True:
        rounds += 1
        update_task(task_dir, round=rounds)
        write_status(task_dir, "running", f"Codex active (round {rounds})")

        returncode, capture = runner.run(current_cmd)
        if capture.thread_id and not thread_id:
            thread_id = capture.thread_id
            update_task(task_dir, thread_id=thread_id)

        question_payload = extract_question(capture.last_assistant_text or "")
        if question_payload and thread_id:
            answer = wait_for_answer(task_dir, question_payload)
            current_cmd = build_resume_cmd(thread_id, build_resume_prompt(answer))
            continue

        if returncode == 0:
            write_result(task_dir, "complete", "Task finished successfully", capture, returncode)
            write_status(task_dir, "complete", "Task finished successfully")
            return

        # If Codex returned non-zero but asked a question and thread_id wasn't detected, record that.
        if question_payload and not thread_id:
            write_result(
                task_dir,
                "error",
                "Codex asked a question but no resumable thread_id was captured",
                capture,
                returncode,
            )
            write_status(task_dir, "error", "Question detected but resume session id missing")
            return

        detail = capture.errors[-1] if capture.errors else f"Codex exited with status {returncode}"
        write_result(task_dir, "error", detail, capture, returncode)
        write_status(task_dir, "error", detail)
        return


def main() -> None:
    parser = argparse.ArgumentParser(description="OpenClaw <-> Codex bridge")
    parser.add_argument("--task-id", required=True, help="Unique task identifier")
    parser.add_argument("--workdir", required=True, help="Working directory for Codex")
    parser.add_argument("--prompt", required=True, help="Prompt to send to Codex")
    args = parser.parse_args()

    if not Path(args.workdir).is_dir():
        print(f"Workdir does not exist: {args.workdir}", file=sys.stderr)
        sys.exit(2)

    def sigterm_handler(signum: int, frame: Any) -> None:
        task_dir = TASKS_DIR / args.task_id
        write_status(task_dir, "error", "Bridge process terminated by signal")
        sys.exit(1)

    signal.signal(signal.SIGTERM, sigterm_handler)
    run_bridge(args.task_id, args.workdir, args.prompt)


if __name__ == "__main__":
    main()


path: codex-bridge-answer.sh
content: #!/usr/bin/env bash
# codex-bridge-answer.sh - Send an answer to a pending Codex bridge question.

set -euo pipefail

BRIDGE_HOME="${HOME}/.codex-bridge"
TASKS_DIR="${BRIDGE_HOME}/tasks"

TASK_ID=""
ANSWER=""

usage() {
    cat <<EOF
Usage: codex-bridge-answer.sh [OPTIONS]

Send an answer to a pending Codex bridge question.

Options:
  -t, --task-id ID      task to answer (required)
  -a, --answer TEXT     answer text (required)
  -h, --help            show this help
EOF
    exit 0
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        -t|--task-id) TASK_ID="$2"; shift 2 ;;
        -a|--answer) ANSWER="$2"; shift 2 ;;
        -h|--help) usage ;;
        *) echo "Unknown option: $1"; usage ;;
    esac
done

if [[ -z "$TASK_ID" ]]; then
    echo "Error: --task-id is required"
    exit 1
fi
if [[ -z "$ANSWER" ]]; then
    echo "Error: --answer is required"
    exit 1
fi

TASK_DIR="$TASKS_DIR/$TASK_ID"
if [[ ! -d "$TASK_DIR" ]]; then
    echo "Error: task '$TASK_ID' not found"
    exit 1
fi

if [[ -f "$TASK_DIR/status.json" ]]; then
    STATUS=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('status',''))" "$TASK_DIR/status.json" 2>/dev/null || echo "")
    if [[ "$STATUS" != "waiting_for_answer" ]]; then
        echo "Warning: task status is '$STATUS', not 'waiting_for_answer'"
    fi
fi

TMP_FILE="$TASK_DIR/answer.json.tmp"
ANSWER_FILE="$TASK_DIR/answer.json"

python3 -c "
import json, sys
from datetime import datetime
data = {'text': sys.argv[1], 'answered_at': datetime.now().isoformat()}
with open(sys.argv[2], 'w') as f:
    json.dump(data, f, indent=2)
" "$ANSWER" "$TMP_FILE"

mv "$TMP_FILE" "$ANSWER_FILE"
echo "Answer sent to task '$TASK_ID'."


path: codex-bridge-dispatch.sh
content: #!/usr/bin/env bash
# codex-bridge-dispatch.sh - Dispatch a task to OpenAI Codex via the local CLI bridge.

set -euo pipefail

SKILL_DIR="$(cd "$(dirname "$0")" && pwd)"
BRIDGE_HOME="${HOME}/.codex-bridge"
TASKS_DIR="${BRIDGE_HOME}/tasks"

TASK_ID=""
WORKDIR="$(pwd)"
PROMPT=""

usage() {
    cat <<EOF
Usage: codex-bridge-dispatch.sh [OPTIONS]

Dispatch a coding task to Codex with status polling and answerable clarifying questions.

Options:
  -t, --task-id NAME    task identifier (default: task-<timestamp>)
  -w, --workdir DIR     working directory for Codex (default: cwd)
  -p, --prompt TEXT     prompt to send to Codex (required)
  -h, --help            show this help

Examples:
  codex-bridge-dispatch.sh -t fix-api -w ~/projects/myapp -p "Fix the null check in api.py"
  codex-bridge-dispatch.sh --prompt "Write a Python CSV parser"
EOF
    exit 0
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        -t|--task-id) TASK_ID="$2"; shift 2 ;;
        -w|--workdir) WORKDIR="$2"; shift 2 ;;
        -p|--prompt)  PROMPT="$2"; shift 2 ;;
        -h|--help)    usage ;;
        *) echo "Unknown option: $1"; usage ;;
    esac
done

if [[ -z "$PROMPT" ]]; then
    echo "Error: --prompt is required"
    exit 1
fi

if ! command -v codex >/dev/null 2>&1; then
    echo "Error: codex CLI not found in PATH"
    echo "Install Codex desktop/CLI and ensure 'codex' is available."
    exit 1
fi

if [[ ! -d "$WORKDIR" ]]; then
    echo "Error: workdir does not exist: $WORKDIR"
    exit 1
fi

if [[ -z "$TASK_ID" ]]; then
    TASK_ID="task-$(date +%Y%m%d-%H%M%S)"
fi

if [[ -d "$TASKS_DIR/$TASK_ID" ]]; then
    echo "Error: task '$TASK_ID' already exists at $TASKS_DIR/$TASK_ID"
    exit 1
fi

mkdir -p "$TASKS_DIR/$TASK_ID"

nohup python3 "$SKILL_DIR/bridge.py" \
    --task-id "$TASK_ID" \
    --workdir "$WORKDIR" \
    --prompt "$PROMPT" \
    >> "$TASKS_DIR/$TASK_ID/bridge.launch.log" 2>&1 &

BRIDGE_PID=$!
echo "$BRIDGE_PID" > "$TASKS_DIR/$TASK_ID/bridge.pid"

sleep 1
if ! kill -0 "$BRIDGE_PID" 2>/dev/null; then
    echo "Error: bridge failed to start. Check: $TASKS_DIR/$TASK_ID/bridge.launch.log"
    exit 1
fi

echo "Task '$TASK_ID' dispatched."
echo ""
echo "  Status:    $SKILL_DIR/codex-bridge-status.sh -t $TASK_ID"
echo "  Output:    $SKILL_DIR/codex-bridge-status.sh -t $TASK_ID --output"
echo "  Question:  $SKILL_DIR/codex-bridge-status.sh -t $TASK_ID --question"
echo "  Answer:    $SKILL_DIR/codex-bridge-answer.sh -t $TASK_ID -a '<text>'"
echo "  Result:    $SKILL_DIR/codex-bridge-status.sh -t $TASK_ID --result"
echo "  Kill:      kill $BRIDGE_PID"
echo "  Log:       $TASKS_DIR/$TASK_ID/bridge.log"


path: codex-bridge-status.sh
content: #!/usr/bin/env bash
# codex-bridge-status.sh - Check status/output for Codex bridge tasks.

set -euo pipefail

BRIDGE_HOME="${HOME}/.codex-bridge"
TASKS_DIR="${BRIDGE_HOME}/tasks"

TASK_ID=""
LIST_ALL=false
SHOW_OUTPUT=false
SHOW_QUESTION=false
SHOW_RESULT=false
SHOW_LOG=false
LINES=50

usage() {
    cat <<EOF
Usage: codex-bridge-status.sh [OPTIONS]

Monitor Codex bridge tasks.

Options:
  -t, --task-id ID      check a specific task
  -l, --list            list all tasks
  -o, --output          show recent output from Codex
  -n, --lines N         number of output lines (default: 50)
  -q, --question        show pending clarifying question
  -r, --result          show task result
      --log             show bridge log
  -h, --help            show this help
EOF
    exit 0
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        -t|--task-id) TASK_ID="$2"; shift 2 ;;
        -l|--list) LIST_ALL=true; shift ;;
        -o|--output) SHOW_OUTPUT=true; shift ;;
        -n|--lines) LINES="$2"; shift 2 ;;
        -q|--question) SHOW_QUESTION=true; shift ;;
        -r|--result) SHOW_RESULT=true; shift ;;
        --log) SHOW_LOG=true; shift ;;
        -h|--help) usage ;;
        *) echo "Unknown option: $1"; usage ;;
    esac
done

if $LIST_ALL; then
    echo "Codex Bridge tasks:"
    echo ""
    if [[ -d "$TASKS_DIR" ]] && [[ "$(ls -A "$TASKS_DIR" 2>/dev/null)" ]]; then
        for task_dir in "$TASKS_DIR"/*/; do
            [[ -d "$task_dir" ]] || continue
            task_name="$(basename "$task_dir")"
            status_file="$task_dir/status.json"
            if [[ -f "$status_file" ]]; then
                status=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print(d.get('status','unknown'))" "$status_file" 2>/dev/null || echo "unknown")
                detail=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print(d.get('detail',''))" "$status_file" 2>/dev/null || echo "")
                echo "  [$task_name]  $status  $detail"
            else
                echo "  [$task_name]  (no status)"
            fi
        done
    else
        echo "  (none)"
    fi
    exit 0
fi

if [[ -z "$TASK_ID" ]]; then
    echo "Error: specify --task-id ID or --list"
    exit 1
fi

TASK_DIR="$TASKS_DIR/$TASK_ID"
if [[ ! -d "$TASK_DIR" ]]; then
    echo "Error: task '$TASK_ID' not found"
    exit 1
fi

if $SHOW_QUESTION; then
    if [[ -f "$TASK_DIR/question.json" ]]; then
        cat "$TASK_DIR/question.json"
    else
        echo "No pending question for task '$TASK_ID'"
    fi
    exit 0
fi

if $SHOW_RESULT; then
    if [[ -f "$TASK_DIR/result.json" ]]; then
        cat "$TASK_DIR/result.json"
    else
        echo "No result yet for task '$TASK_ID'"
    fi
    exit 0
fi

if $SHOW_LOG; then
    if [[ -f "$TASK_DIR/bridge.log" ]]; then
        cat "$TASK_DIR/bridge.log"
    else
        echo "No bridge log for task '$TASK_ID'"
    fi
    exit 0
fi

if $SHOW_OUTPUT; then
    if [[ -f "$TASK_DIR/output.log" ]]; then
        tail -n "$LINES" "$TASK_DIR/output.log"
    else
        echo "No output yet for task '$TASK_ID'"
    fi
    exit 0
fi

STATUS_FILE="$TASK_DIR/status.json"
if [[ ! -f "$STATUS_FILE" ]]; then
    echo "Task: $TASK_ID"
    echo "Status: unknown (no status.json)"
    exit 1
fi

python3 -c "
import json, os, sys
d = json.load(open(sys.argv[1]))
pid = d.get('pid', 0)
alive = False
if pid:
    try:
        os.kill(pid, 0)
        alive = True
    except (ProcessLookupError, PermissionError):
        pass
print(f'Task: {sys.argv[2]}')
print(f'Status: {d.get(\"status\", \"unknown\")}')
print(f'Detail: {d.get(\"detail\", \"\")}')
print(f'Updated: {d.get(\"updated_at\", \"\")}')
if d.get('thread_id'):
    print(f'Thread: {d.get(\"thread_id\")}')
if d.get('round') is not None:
    print(f'Round: {d.get(\"round\")}')
print(f'PID: {pid} ({\"alive\" if alive else \"dead\"})')
if not alive and d.get('status') in ('running', 'starting', 'waiting_for_answer'):
    print(f'WARNING: Process is dead but status shows {d.get(\"status\")}. Check {sys.argv[3]}/bridge.log')
" "$STATUS_FILE" "$TASK_ID" "$TASK_DIR"


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)