Skill: justinzhq/persistent-code-terminal (version 1.2.0)

--- name: persistent-code-terminal description: Persistent per-project coding terminal (tmux). Run Codex CLI (codex exec) inside a stable session; mobile/SSH friendly. user-invocable: true metadata: {"openclaw":{"emoji":"💻","os":["darwin","linux"],"requires":{"bins":["tmux"]},"install":[{"id":"brew","kind":"brew","formula":"tmux","bins":["tmux"],"label":"Install tmux (brew)"},{"id":"apt","kind":"apt","package":"tmux","bins":["tmux"],"label":"Install tmux (apt)"}]}} --- # persistent-code-terminal A **persistent coding terminal** for OpenClaw. It creates a tmux session per project: `<project-name>-code-session` This makes terminal-first AI coding (Codex CLI / Claude Code / any CLI tool) reliable and **mobile/SSH friendly**: - detach/reattach without losing output - keep shell state across turns - keep long-running tasks alive (dev server, watch mode, etc.) ## OpenClaw natural-language routing When user intent is "use natural language to drive Codex CLI in this repo", route through this skill first. Intelligent auto-trigger: - Routing toggle: `openclaw.config.dev.autoCodeRouting` (default `false`). - If enabled, route coding workflow messages via: - `{baseDir}/bin/persistent-code-terminal-route.sh "<user message>"` - The route script applies: - intent detection (code change / fix / test / build / commit / push / codex phrases) - safety filtering (must be git repo + action verbs; bypass on `不要执行，只分析`) - execution chain: - `persistent-code-terminal-auto.sh --max-retries 3 --instruction "<message>"` - `persistent-code-terminal-summary.sh --lines 120` - fallback for missing deps (`tmux`/`codex`) with doctor guidance. Multi-project routing: - One message can include multiple project tasks split by newline / `；` / `;` - Supported patterns: - `给 <project> 项目 <instruction>` - `给<project>项目：<instruction>` - `为 <project> 修复/增加/修改 ...` - `<project> 项目 ...；<project> 项目 ...` - Execution is serial per project: - `start.sh --project <project>` - `auto.sh --max-retries 3 --instruction "<instruction>"` - `summary.sh --lines 120 --json` - Invalid/unknown project tasks are reported as failed without blocking other tasks. Trigger shortcuts (recommended): - If user message starts with `codex `, treat the remaining text as instruction. - Execute: - `{baseDir}/bin/persistent-code-terminal-codex-exec.sh "<remaining text>"` - Then report with: - `{baseDir}/bin/persistent-code-terminal-status.sh` - `{baseDir}/bin/persistent-code-terminal-summary.sh --lines 120` Preferred execution pattern in chat-driven runs: - `start.sh` (or implicit auto-create from `send.sh`) - `codex-exec.sh "<instruction>"` for Codex-driven work - `status.sh` / `summary.sh` for concise progress reporting If user says "检查项目/继续会话/跑测试并总结", use this skill scripts instead of asking user to type long shell commands. ## Core model (must follow) **start → send → read → decide** 1) Start/ensure session: - `{baseDir}/bin/persistent-code-terminal-start.sh` 2) Send ONE command: - `{baseDir}/bin/persistent-code-terminal-send.sh "<command>"` - Appends a pane sentinel on completion: `__PCT_EXIT_CODE__N` - Supports: `--timeout <seconds>`, `--dry-run`, `--phase <name>` 3) Read output: - `{baseDir}/bin/persistent-code-terminal-read.sh` - Parses the latest sentinel and updates `.pct-state.json` 4) Check state quickly (optional): - `{baseDir}/bin/persistent-code-terminal-status.sh` - `{baseDir}/bin/persistent-code-terminal-summary.sh --lines 120` - `{baseDir}/bin/persistent-code-terminal-doctor.sh` - `{baseDir}/bin/persistent-code-terminal-list.sh` (list `*-code-session`) - `{baseDir}/bin/persistent-code-terminal-switch.sh --project <name>` State file: - `.pct-state.json` (current project directory) - Fields: `projectDir`, `session`, `lastCommand`, `lastExitCode`, `phase`, `updatedAt` Structured output: - `{baseDir}/bin/persistent-code-terminal-read.sh --json` - `{baseDir}/bin/persistent-code-terminal-summary.sh --json` - `{baseDir}/bin/persistent-code-terminal-auto.sh --json` ## Codex-first workflow (one-shot) If Codex CLI is installed as `codex`, prefer: - `{baseDir}/bin/persistent-code-terminal-codex-exec.sh "<instruction>"` - Default behavior uses: - `codex exec --full-auto --sandbox workspace-write --cd <current-dir> "<instruction>"` - You can pass additional flags before the instruction: - `{baseDir}/bin/persistent-code-terminal-codex-exec.sh --json -o /tmp/codex.json "<instruction>"` - Set `PCT_CODEX_NO_DEFAULT_FLAGS=1` to disable default flags. Example: - `{baseDir}/bin/persistent-code-terminal-codex-exec.sh "Implement feature X. Ensure build and tests pass. Commit and push to current branch. Do NOT force push."` ## Safety - Never `git push --force` unless user explicitly requests. - Keep secrets out of terminal output. - Prefer feature branches; avoid direct pushes to main/master unless explicitly requested. - For network/privileged actions (for example `git push`), follow active Codex approval/sandbox policy.

path: .github/workflows/ci.yml
content: name: CI

on:
  push:
  pull_request:

jobs:
  quality:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y tmux shellcheck shfmt bats

      - name: Check formatting (shfmt)
        run: shfmt -d bin test

      - name: Lint shell scripts (shellcheck)
        run: shellcheck bin/*.sh

      - name: Run tests (bats)
        run: bats test


path: CHANGELOG.md
content: # Changelog

## 1.2.0
- Added structured JSON mode:
  - `persistent-code-terminal-read.sh --json`
  - `persistent-code-terminal-summary.sh --json`
- Added auto closed-loop runner:
  - `persistent-code-terminal-auto.sh`
- Added multi-project session tools:
  - `persistent-code-terminal-list.sh`
  - `persistent-code-terminal-switch.sh`
  - `persistent-code-terminal-start.sh --project <custom-name>`
- Added intelligent auto-routing entrypoint:
  - `persistent-code-terminal-route.sh`
  - config toggle: `openclaw.config.dev.autoCodeRouting` (default false)
- Added multi-project natural-language routing:
  - parse `给/为/<project> 项目 ...` patterns
  - serial per-project execution with consolidated report
  - invalid project isolation (does not block other tasks)
- Added routing decision logging fields:
  - `detectedMultiProject`, `tasksCount`, `parsedProjects`
- Added route parsing and routing safety tests in Bats

## 1.1.1
- Updated `persistent-code-terminal-codex-exec.sh` to use safer defaults:
  - `--full-auto --sandbox workspace-write --cd <current-dir>`
- Added support for passing custom `codex exec` flags before the instruction
- Added opt-out env var: `PCT_CODEX_NO_DEFAULT_FLAGS=1`
- Updated README to document codex-exec defaults and approval expectations

## 1.1.0
- Added shared shell helpers in `bin/persistent-code-terminal-lib.sh`
- `send/read/attach` now auto-create the tmux session if missing
- Improved `tmux` availability checks and error messaging consistency
- Added Bats tests under `test/`
- Added GitHub Actions CI for `shfmt`, `shellcheck`, and `bats`

## 1.0.0
- Initial release
- Per-project tmux session: <project>-code-session
- start/send/read/attach/kill scripts
- Codex one-shot helper: persistent-code-terminal-codex-exec.sh
- Examples and publishing-ready README


path: README.md
content: # persistent-code-terminal

A tmux-backed **persistent terminal per project** for OpenClaw.

**Primary use:** run **Codex CLI** (`codex exec`) inside a stable session so you can drive “one-sentence → patch/build/test/commit/push” workflows from desktop or phone (SSH), and always be able to **attach** and see what happened.

## Why this skill

- **Continuity:** keep shell state and output in one place (per project).
- **Observability:** attach to the session and watch progress / take over.
- **Mobile friendly:** disconnect/reconnect without losing the run (as long as the machine stays on).

## Business scenarios

- **团队开发提效:** 把“需求一句话 -> 修改 -> 测试 -> 提交”的闭环放进持久会话，减少上下文丢失和重复操作。
- **远程/移动办公:** 手机 SSH 掉线后任务继续跑，回连即可查看完整输出，不需要重来。
- **发布前稳态操作:** 对多项目并行维护场景，可按项目串行执行并汇总结果，降低误操作风险。
- **审计与复盘:** 终端输出、状态文件、路由日志可追踪，便于定位失败原因和复盘流程。

## Architecture

```mermaid
flowchart LR
    U["User / OpenClaw Chat"] --> R["routing (route.sh)"]
    R --> A["auto loop (auto.sh)"]
    A --> C["codex exec (codex-exec.sh)"]
    C --> S["tmux session (<project>-code-session)"]
    S --> IO["send/read/summary/status"]
    IO --> ST[".pct-state.json"]
    R --> L[".pct-routing.log"]
```

## Compared with plain CLI

| 维度 | 普通 CLI 直跑 | persistent-code-terminal |
|---|---|---|
| 连接中断 | 任务常中断 | `tmux` 持续运行 |
| 可观察性 | 依赖当前终端窗口 | 随时 `attach/read/summary` |
| 自动闭环 | 需要人工重试 | `auto.sh` 可自动重试 |
| 多项目 | 手工切换上下文 | `list/switch/route` 支持 |
| 结构化集成 | 输出非结构化 | `read/summary/auto --json` |

Session naming:
- `<project-name>-code-session` (derived from current folder name)

---

## Requirements

- macOS or Linux
- `tmux`
- Optional: Codex CLI installed as `codex`

Install tmux:
- macOS: `brew install tmux`
- Ubuntu/Debian: `sudo apt install tmux`

---

## Installation (OpenClaw)

Place this folder as:

- **Project scope (recommended):** `<your-repo>/skills/persistent-code-terminal`
- **Global scope:** `~/.openclaw/skills/persistent-code-terminal`

OpenClaw skill priority follows the documented order (workspace/project skills override global ones).

---

## OpenClaw natural-language setup (CLI backend)

If you want to chat in natural language and let OpenClaw run Codex CLI through this skill, use this checklist:

1. Install this skill in project scope (`<repo>/skills/persistent-code-terminal`).
2. Ensure `codex` is available in the same shell `PATH` used by OpenClaw.
3. Ensure OpenClaw is allowed to execute local shell commands/tools for the workspace.
4. Run doctor once from repo root:

```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-doctor.sh
```

Doctor will automatically check:
- `tmux` availability
- `codex` availability
- current directory is a git repo
- `tmux` session creation capability

If any check fails, it prints actionable fix commands.

Quick checks:

```bash
command -v codex
command -v tmux
```

Recommended chat wording in OpenClaw:
- "用 persistent-code-terminal 启动会话，然后用 codex 检查项目，不改代码。"
- "用 persistent-code-terminal-send 跑 npm test，超时 120 秒，并给我 summary。"
- "继续上次会话，先看 status，再把失败点修复并提交。"

Recommended shorthand for users:
- Start your message with `codex `, for example:
  - `codex 检查 founder-os 项目，先只读分析，不改代码`
  - `codex 修复当前测试失败，跑测试通过后提交到当前分支`
- In OpenClaw routing, map `codex <instruction>` to:
  - `./skills/persistent-code-terminal/bin/persistent-code-terminal-codex-exec.sh "<instruction>"`

---

## Intelligent auto routing

You can enable auto-routing so coding intents are automatically executed through persistent sessions.

Config toggle (default false):
- `openclaw.config.dev.autoCodeRouting`
- file: `openclaw.config.dev.json`

Enable by setting:

```json
{
  "openclaw": {
    "config": {
      "dev": {
        "autoCodeRouting": true
      }
    }
  }
}
```

Routing entrypoint:

```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-route.sh "<user message>"
```

Behavior:
- Detects coding workflow intent (code changes, fix/build/test/commit/push, codex usage phrases).
- Safety filters:
  - skips auto-run in non-git directories
  - skips when no action verb is detected
  - honors explicit bypass: `不要执行，只分析`
- Keeps Strategy A:
  - `codex ...` / `用 codex ...` / `使用 codex ...` still force codex instruction parsing.
- Executes:
  - `persistent-code-terminal-auto.sh --max-retries 3 --instruction "<message>"`
  - then `persistent-code-terminal-summary.sh --lines 120`
- Fallback:
  - if missing runtime deps (e.g. tmux/codex), suggests running `doctor.sh`.
- Logs routing decision to `.pct-routing.log`:
  - `detectedIntent`, `triggerReason`, `workingDirIsGitRepo`
  - hidden by default; visible with `--debug`.

---

## Multi-project commands (natural language)

Auto routing supports one message with multiple project tasks, for example:

```text
给 A 项目增加 xx 功能；给 B 项目修复 yyyy 并提交
```

Supported project-task patterns include:
- `给 <project> 项目 <instruction>`
- `给<project>项目：<instruction>`
- `为 <project> 增加/修复/修改 ...`
- `<project> 项目 ...；<project> 项目 ...`

Behavior:
- Splits message by newline / `；` / `;`
- Parses project + instruction per segment
- Runs tasks serially (stable order)
- Invalid/unknown projects are marked failed without blocking other tasks
- For each valid project:
  - ensure session exists
  - run `persistent-code-terminal-auto.sh --max-retries 3 --instruction "<instruction>"`
  - collect concise per-project result

Multi-project example:

```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-route.sh "给 project-a 项目增加搜索；给 project-b 项目修复测试失败并提交"
```

## Mobile operation example (phone + SSH)

```bash
# 1) 手机 SSH 到开发机，进入仓库
cd ~/work/project-a

# 2) 触发自动闭环任务
./skills/persistent-code-terminal/bin/persistent-code-terminal-auto.sh \
  --max-retries 3 \
  --instruction "修复当前测试失败，测试通过后提交到当前分支，不要 force push"

# 3) 网络波动后重新连接，继续查看
./skills/persistent-code-terminal/bin/persistent-code-terminal-summary.sh --lines 120
./skills/persistent-code-terminal/bin/persistent-code-terminal-attach.sh
```

## Codex integration case

```bash
# 使用 Codex 在持久会话内完成闭环
./skills/persistent-code-terminal/bin/persistent-code-terminal-codex-exec.sh \
  "实现用户设置页并补测试。确保 build/test 通过后提交到当前分支，不要 force push。"

# 结构化读取结果（供自动化/机器人二次处理）
./skills/persistent-code-terminal/bin/persistent-code-terminal-summary.sh --json
```

---

## Quickstart

From your repo root:

```bash
chmod +x skills/persistent-code-terminal/bin/*.sh
./skills/persistent-code-terminal/bin/persistent-code-terminal-start.sh
./skills/persistent-code-terminal/bin/persistent-code-terminal-attach.sh
```

### Run Codex in the persistent session (one sentence)

```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-codex-exec.sh "Implement: add a settings page with dark mode toggle. Ensure build/test pass. Commit and push to current branch. No force push."
```

Then watch or take over:

```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-attach.sh
```

---

## Example workflows

### 1) Patch → Build → Test → Commit → Push (Codex-driven)

```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-codex-exec.sh "Fix the failing tests. Ensure npm run build and npm test pass. Commit with 'fix:' message and push to current branch. No force push."
```

### 2) Dev server (long running)

```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-start.sh
./skills/persistent-code-terminal/bin/persistent-code-terminal-send.sh "npm run dev"
./skills/persistent-code-terminal/bin/persistent-code-terminal-attach.sh
```

Detach: `Ctrl+b` then `d`. Reattach any time.

---

## Script behavior

- `start.sh`: create the session if missing, otherwise reuse it.
  - `--project <custom-name>`: use `<custom-name>-code-session` for this start call.
- `send.sh`: auto-creates the session if missing, wraps the command with `bash -lc`, and prints `__PCT_EXIT_CODE__N` to pane output when done.
- `read.sh`: auto-creates the session if missing, prints recent pane output, and updates `lastExitCode` from the latest sentinel.
  - `--json`: machine-readable payload with state metadata + `recentOutput`.
- `attach.sh`: auto-creates the session if missing, then attaches.
- `codex-exec.sh`: by default runs:
  - `codex exec --full-auto --sandbox workspace-write --cd <current-dir> "<instruction>"`
  - Pass extra flags before the instruction, for example:
    - `./skills/persistent-code-terminal/bin/persistent-code-terminal-codex-exec.sh --json -o /tmp/codex.json "Implement feature X"`
  - Set `PCT_CODEX_NO_DEFAULT_FLAGS=1` to disable default flags.
- `status.sh`: prints project/session status + last command + last exit code + git clean/dirty.
- `summary.sh`: prints compact header + recent output (`--lines`, default `120`) + error grep section.
  - `--json`: machine-readable payload with state metadata + `recentOutput` + `errorLines`.
- `doctor.sh`: checks tmux/bash/codex availability and tmux session creation ability.
- `list.sh`: list active `*-code-session` tmux sessions (supports `--json`).
- `switch.sh`: attach to an existing session via `--project` or `--session` (does not auto-create).

State file:
- `.pct-state.json` in the current project directory.
- Updated by `start/send/read` with: `projectDir`, `session`, `lastCommand`, `lastExitCode`, `phase`, `updatedAt`.

---

## Send options

- `--dry-run`: print the wrapped command that would be sent; does not send to tmux.
- `--timeout <seconds>`: send command and poll until `__PCT_EXIT_CODE__N` appears, else exit `124`.
- `--phase <name>`: store a lightweight phase label in `.pct-state.json`.

Examples:

```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-send.sh --timeout 10 "npm test"
./skills/persistent-code-terminal/bin/persistent-code-terminal-summary.sh --lines 80
./skills/persistent-code-terminal/bin/persistent-code-terminal-status.sh
```

```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-send.sh --dry-run "echo hello"
./skills/persistent-code-terminal/bin/persistent-code-terminal-doctor.sh
```

---

## JSON output mode

For automation/integration, both `read.sh` and `summary.sh` support `--json`:

```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-read.sh --json
./skills/persistent-code-terminal/bin/persistent-code-terminal-summary.sh --lines 120 --json
```

Notes:
- JSON mode prints strict JSON only (no human-readable headers).
- Metadata comes from `.pct-state.json` (`projectDir`, `session`, `lastCommand`, `lastExitCode`, `phase`, `updatedAt`).
- `summary --json` includes `errorLines` (matches case-insensitive `error|failed|exception`).

---

## Auto closed-loop mode

Use `persistent-code-terminal-auto.sh` to run Codex in a retry loop:

```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-auto.sh \
  --max-retries 3 \
  --instruction "Fix failing tests and push to current branch"
```

Options:
- `--max-retries <N>`: max attempts (default `3`)
- `--instruction "<text>"`: required initial instruction
- `--timeout <seconds>`: per-attempt timeout (passed to `send.sh` via `codex-exec.sh`)
- `--json`: strict structured output

JSON output shape:
- `success` (boolean)
- `attempts` (number)
- `finalExitCode` (number|null)
- `lastCommand` (string)
- `phase` (string|null)

---

## Multi-project sessions

List active project sessions:

```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-list.sh
./skills/persistent-code-terminal/bin/persistent-code-terminal-list.sh --json
```

Switch to another project session:

```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-switch.sh --project project-a
./skills/persistent-code-terminal/bin/persistent-code-terminal-switch.sh --session project-a-code-session
```

Start with custom project session name:

```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-start.sh --project project-a
```

Example workflow:

```bash
# In project A
./skills/persistent-code-terminal/bin/persistent-code-terminal-start.sh

# In project B
./skills/persistent-code-terminal/bin/persistent-code-terminal-start.sh

# List all
./skills/persistent-code-terminal/bin/persistent-code-terminal-list.sh

# Switch
./skills/persistent-code-terminal/bin/persistent-code-terminal-switch.sh --project project-a
```

---

## Quality checks

This repo includes CI for:
- `shfmt -d` (format check)
- `shellcheck` (lint)
- `bats` (script behavior tests)

---

## Case study (realistic)

See: `examples/case-study-patch-to-push.md`

---

## Security notes

This skill runs local scripts. Review the `bin/` scripts before installing from third parties. Avoid putting secrets in commands or logs.
Tasks that require network or privileged actions (for example `git push`) may still require explicit approval/policy changes depending on your Codex CLI sandbox settings.

---

## License

MIT (see LICENSE if you add it for ClawHub publishing).


path: RELEASE.md
content: # Release Guide

## 1.1.1

### Summary

This release aligns `persistent-code-terminal` with recommended Codex CLI non-interactive usage and improves documentation consistency.

### Highlights

- `persistent-code-terminal-codex-exec.sh` now defaults to:
  - `codex exec --full-auto --sandbox workspace-write --cd <current-dir> "<instruction>"`
- Supports passing extra `codex exec` flags before the instruction
  - Example: `--json -o /tmp/codex-run.json`
- Added opt-out env var:
  - `PCT_CODEX_NO_DEFAULT_FLAGS=1`
- Updated docs to reflect default behavior and approval/sandbox expectations

### User-visible behavior changes

- Running:
  - `./bin/persistent-code-terminal-codex-exec.sh "Do X"`
- Now executes Codex with default automation-oriented flags unless opted out.

### Compatibility notes

- No breaking changes to existing script entrypoints.
- Existing calls with only an instruction continue to work.
- Calls that already pass explicit flags are now supported directly.

### Validation checklist

Run locally (if tools are installed):

```bash
bash -n bin/*.sh
shfmt -d bin test
shellcheck bin/*.sh
bats test
```

Or rely on CI (`.github/workflows/ci.yml`) for `shfmt`, `shellcheck`, and `bats`.

### Suggested tag message

`v1.1.1: codex-exec defaults to full-auto/workspace-write, plus docs alignment`

---

## 1.2.0 Release Checklist

### Pre-flight

1. Ensure toolchain is available:
   - `tmux`, `bash`, `shellcheck`, `shfmt`, `bats`
2. Run quality checks:
   - `bash -n bin/*.sh`
   - `shfmt -d bin test`
   - `shellcheck bin/*.sh`
   - `bats test`
3. Smoke test key workflows:
   - `start/send/read/summary/status/doctor`
   - `read --json` and `summary --json`
   - `auto --instruction "..."`
   - `list/switch` with at least two sessions
   - `route` single-project and multi-project message

### Versioning

1. Confirm `CHANGELOG.md` includes `1.2.0` entry.
2. Confirm docs updated (`README.md`, `SKILL.md`).

### Publish

1. Commit release changes.
2. Create annotated tag:
   - `git tag -a v1.2.0 -m "v1.2.0: routing, multi-project, json, auto-loop"`
3. Push branch and tag:
   - `git push`
   - `git push origin v1.2.0`
4. Create GitHub release notes from `CHANGELOG.md`.


path: bin/persistent-code-terminal-attach.sh
content: #!/usr/bin/env bash
set -euo pipefail
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=bin/persistent-code-terminal-lib.sh
source "$BASE_DIR/persistent-code-terminal-lib.sh"
SESSION="$(session_name)"

ensure_session
tmux attach -t "$SESSION"


path: bin/persistent-code-terminal-auto.sh
content: #!/usr/bin/env bash
set -euo pipefail
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=bin/persistent-code-terminal-lib.sh
source "$BASE_DIR/persistent-code-terminal-lib.sh"

MAX_RETRIES=3
INSTRUCTION=""
TIMEOUT=""
JSON_MODE=0

usage() {
  cat <<'EOF_USAGE'
Usage:
  persistent-code-terminal-auto.sh --instruction "<text>" [--max-retries <N>] [--timeout <seconds>] [--json]
EOF_USAGE
}

while [ "$#" -gt 0 ]; do
  case "$1" in
    --max-retries)
      if [ "$#" -lt 2 ]; then
        echo "--max-retries requires a value." >&2
        exit 1
      fi
      MAX_RETRIES="$2"
      shift 2
      ;;
    --instruction)
      if [ "$#" -lt 2 ]; then
        echo "--instruction requires a value." >&2
        exit 1
      fi
      INSTRUCTION="$2"
      shift 2
      ;;
    --timeout)
      if [ "$#" -lt 2 ]; then
        echo "--timeout requires a value." >&2
        exit 1
      fi
      TIMEOUT="$2"
      shift 2
      ;;
    --json)
      JSON_MODE=1
      shift
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      echo "Unknown option: $1" >&2
      usage >&2
      exit 1
      ;;
  esac
done

if [ -z "$INSTRUCTION" ]; then
  echo "--instruction is required." >&2
  usage >&2
  exit 1
fi

if ! [[ "$MAX_RETRIES" =~ ^[1-9][0-9]*$ ]]; then
  echo "--max-retries must be a positive integer." >&2
  exit 1
fi

if [ -n "$TIMEOUT" ] && ! [[ "$TIMEOUT" =~ ^[0-9]+$ ]]; then
  echo "--timeout must be an integer number of seconds." >&2
  exit 1
fi

parse_last_exit_from_read_json() {
  awk -F: '/"lastExitCode"/ {gsub(/[ ,]/, "", $2); print $2; exit}'
}

wait_for_completion() {
  local timeout="${1:-}"
  local start_ts read_json exit_token

  start_ts=$SECONDS
  while :; do
    read_json="$("$BASE_DIR/persistent-code-terminal-read.sh" --json)"
    exit_token="$(printf '%s\n' "$read_json" | parse_last_exit_from_read_json)"
    if [ -n "$exit_token" ] && [ "$exit_token" != "null" ]; then
      printf '%s\n' "$exit_token"
      return 0
    fi

    if [ -n "$timeout" ] && [ $((SECONDS - start_ts)) -ge "$timeout" ]; then
      printf '124\n'
      return 124
    fi
    sleep 1
  done
}

if [ "$JSON_MODE" -eq 1 ]; then
  "$BASE_DIR/persistent-code-terminal-start.sh" >/dev/null
else
  "$BASE_DIR/persistent-code-terminal-start.sh" >/dev/null
fi

attempt=0
success=false
final_exit_code="null"
current_instruction="$INSTRUCTION"

while [ "$attempt" -lt "$MAX_RETRIES" ]; do
  attempt=$((attempt + 1))

  if [ -n "$TIMEOUT" ]; then
    if ! PCT_SEND_TIMEOUT="$TIMEOUT" PCT_SEND_PHASE="attempt-$attempt" "$BASE_DIR/persistent-code-terminal-codex-exec.sh" "$current_instruction" >/dev/null 2>&1; then
      :
    fi
  else
    if ! PCT_SEND_PHASE="attempt-$attempt" "$BASE_DIR/persistent-code-terminal-codex-exec.sh" "$current_instruction" >/dev/null 2>&1; then
      :
    fi
  fi

  if [ -n "$TIMEOUT" ]; then
    if exit_value="$(wait_for_completion "$TIMEOUT")"; then
      final_exit_code="$exit_value"
    else
      final_exit_code="124"
    fi
  else
    exit_value="$(wait_for_completion "")"
    final_exit_code="$exit_value"
  fi

  if [ "$final_exit_code" = "0" ]; then
    success=true
    break
  fi

  current_instruction="The previous attempt failed with exit code $final_exit_code. Fix the issues and ensure build/test pass. Do not force push. Do not switch branches. Then rerun necessary checks."
done

LAST_COMMAND="$(pct_state_read_string "lastCommand")"
PHASE="$(pct_state_read_string "phase")"

if [ "$JSON_MODE" -eq 1 ]; then
  cat <<EOF_JSON
{
  "success": ${success},
  "attempts": ${attempt},
  "finalExitCode": ${final_exit_code},
  "lastCommand": "$(pct_json_escape "$LAST_COMMAND")",
  "phase": $(pct_json_quote_or_null "$PHASE")
}
EOF_JSON
else
  if [ "$success" = true ]; then
    RESULT="success"
  else
    RESULT="failure"
  fi
  echo "Auto loop: $RESULT"
  echo "Attempts: $attempt/$MAX_RETRIES"
  echo "Final exit code: $final_exit_code"
fi

if [ "$success" = true ]; then
  exit 0
fi

if [ "$final_exit_code" = "null" ]; then
  exit 1
fi
exit "$final_exit_code"


path: bin/persistent-code-terminal-codex-exec.sh
content: #!/usr/bin/env bash
set -euo pipefail

BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
SESSION="$("$BASE_DIR/persistent-code-terminal-session-name.sh")"
INSTR=""
USER_FLAGS=()

usage() {
  cat <<'EOF'
Usage:
  persistent-code-terminal-codex-exec.sh "<instruction>"
  persistent-code-terminal-codex-exec.sh [codex exec flags...] "<instruction>"

Behavior:
  - By default, this script runs:
      codex exec --full-auto --sandbox workspace-write --cd <current-dir> "<instruction>"
  - Set PCT_CODEX_NO_DEFAULT_FLAGS=1 to disable default flags.

Examples:
  persistent-code-terminal-codex-exec.sh "Fix tests and commit."
  persistent-code-terminal-codex-exec.sh --json -o /tmp/codex.json "Implement feature X."
EOF
}

if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
  usage
  exit 0
fi

if [ "$#" -lt 1 ]; then
  usage
  exit 1
fi

if [ "$#" -eq 1 ]; then
  INSTR="$1"
else
  INSTR="${!#}"
  USER_FLAGS=("${@:1:$#-1}")
fi

if [ -z "$INSTR" ]; then
  usage
  exit 1
fi

command -v codex >/dev/null 2>&1 || {
  echo "codex not found. Install Codex CLI first (command name must be 'codex')."
  exit 1
}

DEFAULT_FLAGS=()
if [ "${PCT_CODEX_NO_DEFAULT_FLAGS:-0}" != "1" ]; then
  DEFAULT_FLAGS=(--full-auto --sandbox workspace-write --cd "$(pwd)")
fi

CMD=(codex exec "${DEFAULT_FLAGS[@]}" "${USER_FLAGS[@]}" "$INSTR")
CMD_STR=""
for arg in "${CMD[@]}"; do
  CMD_STR+="$(printf "%q " "$arg")"
done
CMD_STR="${CMD_STR% }"

# Ensure session exists
"$BASE_DIR/persistent-code-terminal-start.sh"

# Run Codex one-shot inside the persistent session
SEND_ARGS=()
if [ -n "${PCT_SEND_TIMEOUT:-}" ]; then
  SEND_ARGS+=(--timeout "$PCT_SEND_TIMEOUT")
fi
if [ -n "${PCT_SEND_PHASE:-}" ]; then
  SEND_ARGS+=(--phase "$PCT_SEND_PHASE")
fi

"$BASE_DIR/persistent-code-terminal-send.sh" "${SEND_ARGS[@]}" "$CMD_STR"
"$BASE_DIR/persistent-code-terminal-read.sh"

echo "Codex started in session: $SESSION"
echo "Tip: $BASE_DIR/persistent-code-terminal-attach.sh to watch progress."


path: bin/persistent-code-terminal-doctor.sh
content: #!/usr/bin/env bash
set -euo pipefail

PASS=1
WARN=0

print_fix() {
  echo "  Fix: $1"
}

if command -v tmux >/dev/null 2>&1; then
  echo "[PASS] tmux installed"
else
  echo "[FAIL] tmux missing"
  print_fix "macOS: brew install tmux"
  print_fix "Ubuntu/Debian: sudo apt install tmux"
  PASS=0
fi

if command -v bash >/dev/null 2>&1; then
  echo "[PASS] bash available"
else
  echo "[FAIL] bash missing"
  PASS=0
fi

if command -v codex >/dev/null 2>&1; then
  echo "[PASS] codex available"
else
  echo "[FAIL] codex missing"
  print_fix "Install Codex CLI and ensure command 'codex' is in PATH"
  print_fix "Then verify: command -v codex"
  PASS=0
fi

if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  echo "[PASS] current directory is a git repo"
else
  echo "[FAIL] current directory is not a git repo"
  print_fix "cd <your-repo-root>"
  print_fix "or initialize repo: git init"
  PASS=0
fi

if [ "$PASS" -eq 1 ]; then
  TMP_SESSION="pct-doctor-$$"
  if tmux new-session -d -s "$TMP_SESSION" -c "$(pwd)" >/dev/null 2>&1; then
    tmux kill-session -t "$TMP_SESSION" >/dev/null 2>&1 || true
    echo "[PASS] tmux can create sessions"
  else
    echo "[FAIL] tmux cannot create a session in this environment"
    print_fix "Check tmux permissions and run inside a writable project directory"
    PASS=0
  fi
fi

if [ "$PASS" -eq 1 ]; then
  echo "Doctor result: healthy (ready for persistent-code-terminal + Codex workflow)"
  exit 0
fi

if [ "$WARN" -eq 1 ]; then
  echo "Doctor result: warnings found"
else
  echo "Doctor result: not healthy"
fi
exit 1


path: bin/persistent-code-terminal-kill.sh
content: #!/usr/bin/env bash
set -euo pipefail
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=bin/persistent-code-terminal-lib.sh
source "$BASE_DIR/persistent-code-terminal-lib.sh"
SESSION="$(session_name)"

require_tmux

if has_session; then
  tmux kill-session -t "$SESSION"
  echo "Killed session: $SESSION"
else
  echo "No session found: $SESSION"
fi


path: bin/persistent-code-terminal-lib.sh
content: #!/usr/bin/env bash
set -euo pipefail

base_dir() {
  cd "$(dirname "${BASH_SOURCE[0]}")" && pwd
}

normalize_project_name() {
  printf '%s' "$1" | tr -cs '[:alnum:]' '-'
}

session_name_from_project() {
  local project_name
  project_name="$(normalize_project_name "$1")"
  printf '%s-code-session\n' "$project_name"
}

session_name() {
  session_name_from_project "$(basename "$(pwd)")"
}

require_tmux() {
  if ! command -v tmux >/dev/null 2>&1; then
    echo "tmux not found. Install tmux first." >&2
    exit 1
  fi
}

has_session() {
  local session
  session="$(session_name)"
  tmux has-session -t "$session" 2>/dev/null
}

ensure_session() {
  local session
  session="$(session_name)"
  require_tmux

  if has_session; then
    return 0
  fi

  tmux new-session -d -s "$session" -c "$(pwd)"
  echo "Created session: $session"
}

pct_now_utc() {
  date -u +"%Y-%m-%dT%H:%M:%SZ"
}

pct_state_path() {
  printf '%s/.pct-state.json\n' "$(pwd)"
}

pct_json_escape() {
  printf '%s' "$1" | awk '
    BEGIN { first = 1 }
    {
      gsub(/\\/, "\\\\")
      gsub(/"/, "\\\"")
      gsub(/\r/, "\\r")
      gsub(/\t/, "\\t")
      if (first == 0) {
        printf "\\n"
      }
      printf "%s", $0
      first = 0
    }
  '
}

pct_json_quote_or_null() {
  if [ -z "${1:-}" ]; then
    printf 'null'
    return 0
  fi

  printf '"%s"' "$(pct_json_escape "$1")"
}

pct_state_read_token() {
  local field="$1"
  local file
  file="$(pct_state_path)"

  if [ ! -f "$file" ]; then
    return 1
  fi

  awk -v key="\"$field\"" '
    $1 == key {
      sub(/^[^:]*:[[:space:]]*/, "")
      sub(/[[:space:]]*,?[[:space:]]*$/, "")
      print
      exit
    }
  ' "$file"
}

pct_state_read_string() {
  local field="$1"
  local token

  [REDACTED_SECRET] "$field" 2>/dev/null || true)"
  if [ -z "$token" ] || [ "$token" = "null" ]; then
    printf ''
    return 0
  fi

  [REDACTED_SECRET]"}"
  [REDACTED_SECRET]"}"
  [REDACTED_SECRET]'\n'}"
  [REDACTED_SECRET]'\r'}"
  [REDACTED_SECRET]'\t'}"
  [REDACTED_SECRET]"/\"}"
  [REDACTED_SECRET]"
  printf '%s' "$token"
}

pct_state_write() {
  local project_dir="$1"
  local session="$2"
  local last_command="$3"
  local last_exit_code_token="$4"
  local phase="$5"
  local updated_at="$6"
  local state_file
  local phase_json

  state_file="$(pct_state_path)"

  if [ -z "$last_exit_code_token" ]; then
    last_exit_code_token="null"
  fi

  if [ -z "$phase" ]; then
    phase_json="null"
  else
    phase_json="\"$(pct_json_escape "$phase")\""
  fi

  cat >"$state_file" <<EOF_STATE
{
  "projectDir": "$(pct_json_escape "$project_dir")",
  "session": "$(pct_json_escape "$session")",
  "lastCommand": "$(pct_json_escape "$last_command")",
  "lastExitCode": ${last_exit_code_token},
  "phase": ${phase_json},
  "updatedAt": "$(pct_json_escape "$updated_at")"
}
EOF_STATE
}

pct_state_update_start() {
  local session_override="${1:-}"
  local project_dir
  local session
  local updated_at
  local last_command
  local last_exit
  local phase

  project_dir="$(pwd -P)"
  if [ -n "$session_override" ]; then
    session="$session_override"
  else
    session="$(session_name)"
  fi
  updated_at="$(pct_now_utc)"
  last_command="$(pct_state_read_string "lastCommand")"
  last_exit="$(pct_state_read_token "lastExitCode" 2>/dev/null || true)"
  phase="$(pct_state_read_string "phase")"

  if [ -z "$last_exit" ]; then
    last_exit="null"
  fi

  pct_state_write "$project_dir" "$session" "$last_command" "$last_exit" "$phase" "$updated_at"
}

pct_state_update_send() {
  local command="$1"
  local phase="${2:-}"
  local project_dir
  local session
  local updated_at

  project_dir="$(pwd -P)"
  session="$(session_name)"
  updated_at="$(pct_now_utc)"

  pct_state_write "$project_dir" "$session" "$command" "null" "$phase" "$updated_at"
}

pct_state_update_read_exit() {
  local maybe_exit="$1"
  local project_dir
  local session
  local updated_at
  local last_command
  local last_exit
  local phase

  project_dir="$(pwd -P)"
  session="$(session_name)"
  updated_at="$(pct_now_utc)"
  last_command="$(pct_state_read_string "lastCommand")"
  phase="$(pct_state_read_string "phase")"

  if [ -n "$maybe_exit" ]; then
    last_exit="$maybe_exit"
  else
    last_exit="$(pct_state_read_token "lastExitCode" 2>/dev/null || true)"
    if [ -z "$last_exit" ]; then
      last_exit="null"
    fi
  fi

  pct_state_write "$project_dir" "$session" "$last_command" "$last_exit" "$phase" "$updated_at"
}


path: bin/persistent-code-terminal-list.sh
content: #!/usr/bin/env bash
set -euo pipefail
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=bin/persistent-code-terminal-lib.sh
source "$BASE_DIR/persistent-code-terminal-lib.sh"
JSON_MODE=0

while [ "$#" -gt 0 ]; do
  case "$1" in
    --json)
      JSON_MODE=1
      shift
      ;;
    -h|--help)
      echo "Usage: persistent-code-terminal-list.sh [--json]"
      exit 0
      ;;
    *)
      echo "Unknown option: $1" >&2
      exit 1
      ;;
  esac
done

require_tmux
SESSIONS_RAW="$(tmux list-sessions -F '#{session_name}|#{?session_attached,1,0}' 2>/dev/null || true)"
FILTERED="$(printf '%s\n' "$SESSIONS_RAW" | awk -F'|' '$1 ~ /-code-session$/ {print $0}')"

if [ "$JSON_MODE" -eq 1 ]; then
  JSON_OUT="["
  FIRST=1
  while IFS='|' read -r session attached; do
    [ -z "$session" ] && continue
    project="${session%-code-session}"
    if [ "$FIRST" -eq 0 ]; then
      JSON_OUT+=", "
    fi
    if [ "$attached" = "1" ]; then
      attached_json="true"
    else
      attached_json="false"
    fi
    JSON_OUT+="{\"project\":\"$(pct_json_escape "$project")\",\"session\":\"$(pct_json_escape "$session")\",\"attached\":${attached_json}}"
    FIRST=0
  done <<EOF_SESS
$FILTERED
EOF_SESS
  JSON_OUT+="]"
  printf '%s\n' "$JSON_OUT"
  exit 0
fi

printf '%s\n' 'PROJECT_NAME | SESSION_NAME | STATUS'
while IFS='|' read -r session attached; do
  [ -z "$session" ] && continue
  project="${session%-code-session}"
  if [ "$attached" = "1" ]; then
    status="attached"
  else
    status="detached"
  fi
  printf '%s | %s | %s\n' "$project" "$session" "$status"
done <<EOF_SESS
$FILTERED
EOF_SESS


path: bin/persistent-code-terminal-read.sh
content: #!/usr/bin/env bash
set -euo pipefail
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=bin/persistent-code-terminal-lib.sh
source "$BASE_DIR/persistent-code-terminal-lib.sh"
SESSION="$(session_name)"
JSON_MODE=0

while [ "$#" -gt 0 ]; do
  case "$1" in
    --json)
      JSON_MODE=1
      shift
      ;;
    -h|--help)
      echo "Usage: persistent-code-terminal-read.sh [--json]"
      exit 0
      ;;
    *)
      echo "Unknown option: $1" >&2
      exit 1
      ;;
  esac
done

if [ "$JSON_MODE" -eq 1 ]; then
  ensure_session >/dev/null
else
  ensure_session
fi

CAPTURED="$(tmux capture-pane -pt "$SESSION" -S -2000)"
LATEST_EXIT="$(printf '%s\n' "$CAPTURED" | sed -n 's/.*__PCT_EXIT_CODE__\([0-9][0-9]*\).*/\1/p' | tail -n 1)"
pct_state_update_read_exit "$LATEST_EXIT"

if [ "$JSON_MODE" -eq 0 ]; then
  printf '%s\n' "$CAPTURED"
  exit 0
fi

PROJECT_DIR="$(pct_state_read_string "projectDir")"
STATE_SESSION="$(pct_state_read_string "session")"
LAST_COMMAND="$(pct_state_read_string "lastCommand")"
PHASE="$(pct_state_read_string "phase")"
UPDATED_AT="$(pct_state_read_string "updatedAt")"

if [ -z "$PROJECT_DIR" ]; then
  PROJECT_DIR="$(pwd -P)"
fi
if [ -z "$STATE_SESSION" ]; then
  STATE_SESSION="$SESSION"
fi
if [ -z "$UPDATED_AT" ]; then
  UPDATED_AT="$(pct_now_utc)"
fi

if [ -n "$LATEST_EXIT" ]; then
  LAST_EXIT_CODE="$LATEST_EXIT"
else
  LAST_EXIT_CODE="null"
fi

cat <<EOF_JSON
{
  "projectDir": "$(pct_json_escape "$PROJECT_DIR")",
  "session": "$(pct_json_escape "$STATE_SESSION")",
  "lastCommand": "$(pct_json_escape "$LAST_COMMAND")",
  "lastExitCode": ${LAST_EXIT_CODE},
  "phase": $(pct_json_quote_or_null "$PHASE"),
  "updatedAt": "$(pct_json_escape "$UPDATED_AT")",
  "recentOutput": "$(pct_json_escape "$CAPTURED")"
}
EOF_JSON


path: bin/persistent-code-terminal-route.sh
content: #!/usr/bin/env bash
set -euo pipefail
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=bin/persistent-code-terminal-lib.sh
source "$BASE_DIR/persistent-code-terminal-lib.sh"

DEBUG=0
INPUT_TEXT="${1:-}"
PARSE_ONLY=0
JSON_MODE=0

usage() {
  cat <<'EOF_USAGE'
Usage:
  persistent-code-terminal-route.sh [--debug] [--parse-only] [--json] "<user-message>"
EOF_USAGE
}

while [ "$#" -gt 0 ]; do
  case "$1" in
    --debug)
      DEBUG=1
      shift
      ;;
    --parse-only)
      PARSE_ONLY=1
      shift
      ;;
    --json)
      JSON_MODE=1
      shift
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      INPUT_TEXT="$1"
      shift
      ;;
  esac
done

if [ -z "${INPUT_TEXT:-}" ]; then
  echo "No input message provided." >&2
  usage >&2
  exit 1
fi

log_decision() {
  local detected="$1"
  local reason="$2"
  local is_git="$3"
  local detected_multi="${4:-false}"
  local tasks_count="${5:-0}"
  local parsed_projects="${6:-}"
  local line

  line="{\"ts\":\"$(pct_now_utc)\",\"detectedIntent\":${detected},\"triggerReason\":\"$(pct_json_escape "$reason")\",\"workingDirIsGitRepo\":${is_git},\"detectedMultiProject\":${detected_multi},\"tasksCount\":${tasks_count},\"parsedProjects\":\"$(pct_json_escape "$parsed_projects")\"}"
  printf '%s\n' "$line" >>"$(pwd)/.pct-routing.log"
  if [ "$DEBUG" -eq 1 ]; then
    printf '%s\n' "$line"
  fi
}

strip_prefix_instruction() {
  printf '%s' "$1" | sed -E 's/^[[:space:]]*(codex[[:space:]]+|用[[:space:]]*codex[[:space:]]+|使用[[:space:]]*codex[[:space:]]+)//'
}

contains_any() {
  local text="$1"
  shift
  local pattern
  for pattern in "$@"; do
    if printf '%s' "$text" | grep -Eiq "$pattern"; then
      return 0
    fi
  done
  return 1
}

trim_spaces() {
  printf '%s' "$1" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//'
}

read_auto_code_routing_flag() {
  local config_file value

  if [ -n "${OPENCLAW_CONFIG_DEV_AUTO_CODE_ROUTING:-}" ]; then
    value="${OPENCLAW_CONFIG_DEV_AUTO_CODE_ROUTING}"
    case "$value" in
      true|TRUE|1|yes|YES) echo "true"; return 0 ;;
      *) echo "false"; return 0 ;;
    esac
  fi

  config_file="$(pwd)/openclaw.config.dev.json"
  if [ -f "$config_file" ]; then
    value="$(sed -n 's/.*"autoCodeRouting"[[:space:]]*:[[:space:]]*\(true\|false\).*/\1/p' "$config_file" | head -n 1)"
    if [ "$value" = "true" ]; then
      echo "true"
      return 0
    fi
  fi

  echo "false"
}

project_dir_from_name() {
  local project="$1"
  local cur base parent session path

  cur="$(pwd -P)"
  base="$(basename "$cur")"
  if [ "$project" = "$base" ]; then
    printf '%s\n' "$cur"
    return 0
  fi

  if [ -d "$cur/$project" ]; then
    cd "$cur/$project" && pwd -P
    return 0
  fi

  parent="$(cd "$cur/.." && pwd -P)"
  if [ -d "$parent/$project" ]; then
    cd "$parent/$project" && pwd -P
    return 0
  fi

  session="$(session_name_from_project "$project")"
  if command -v tmux >/dev/null 2>&1 && tmux has-session -t "$session" 2>/dev/null; then
    path="$(tmux display-message -p -t "$session" '#{pane_current_path}' 2>/dev/null || true)"
    if [ -n "$path" ] && [ -d "$path" ]; then
      cd "$path" && pwd -P
      return 0
    fi
  fi

  return 1
}

is_known_project() {
  local project="$1"
  if project_dir_from_name "$project" >/dev/null 2>&1; then
    return 0
  fi
  return 1
}

split_segments() {
  printf '%s' "$1" | sed 's/；/;/g' | tr '\n' ';' | tr ';' '\n'
}

parse_segment() {
  local seg="$1"
  local project=""
  local instruction=""

  seg="$(trim_spaces "$seg")"
  if [ -z "$seg" ]; then
    return 1
  fi

  if printf '%s' "$seg" | grep -Eq '^[给为对][[:space:]]*[[:alnum:]_.-]+[[:space:]]*项目'; then
    project="$(printf '%s' "$seg" | sed -E 's/^[给为对][[:space:]]*([[:alnum:]_.-]+)[[:space:]]*项目[:：]?[[:space:]]*(.*)$/\1/' | head -n 1)"
    instruction="$(printf '%s' "$seg" | sed -E 's/^[给为对][[:space:]]*([[:alnum:]_.-]+)[[:space:]]*项目[:：]?[[:space:]]*(.*)$/\2/' | head -n 1)"
  elif printf '%s' "$seg" | grep -Eq '^[给为对][[:space:]]*[[:alnum:]_.-]+[[:space:]]+'; then
    project="$(printf '%s' "$seg" | sed -E 's/^[给为对][[:space:]]*([[:alnum:]_.-]+)[[:space:]]+(.*)$/\1/' | head -n 1)"
    instruction="$(printf '%s' "$seg" | sed -E 's/^[给为对][[:space:]]*([[:alnum:]_.-]+)[[:space:]]+(.*)$/\2/' | head -n 1)"
  elif printf '%s' "$seg" | grep -Eq '^[[:alnum:]_.-]+[[:space:]]*项目'; then
    project="$(printf '%s' "$seg" | sed -E 's/^([[:alnum:]_.-]+)[[:space:]]*项目[:：]?[[:space:]]*(.*)$/\1/' | head -n 1)"
    instruction="$(printf '%s' "$seg" | sed -E 's/^([[:alnum:]_.-]+)[[:space:]]*项目[:：]?[[:space:]]*(.*)$/\2/' | head -n 1)"
  fi

  if [ -n "$project" ]; then
    instruction="$(trim_spaces "$instruction")"
    printf '%s|%s\n' "$project" "$instruction"
    return 0
  fi
  return 1
}

json_get_scalar() {
  local json="$1"
  local key="$2"
  printf '%s\n' "$json" | awk -v k="\"$key\"" '
    {
      if (match($0, k "[[:space:]]*:[[:space:]]*")) {
        rest = substr($0, RSTART + RLENGTH)
        if (substr(rest, 1, 1) == "\"") {
          sub(/^"/, "", rest)
          sub(/".*$/, "", rest)
          print rest
          exit
        }
        gsub(/[[:space:]]/, "", rest)
        sub(/[,}].*$/, "", rest)
        print rest
        exit
      }
    }
  '
}

json_get_first_error_line() {
  local json="$1"
  printf '%s\n' "$json" | awk '
    {
      if (match($0, /"errorLines"[[:space:]]*:[[:space:]]*\[/)) {
        rest = substr($0, RSTART + RLENGTH)
        if (match(rest, /^"[^"]*"/)) {
          first = substr(rest, RSTART + 1, RLENGTH - 2)
          print first
        }
        exit
      }
    }
  '
}

ACTION_PATTERNS=(
  '增加|新增|实现|添加'
  '修改|重构|优化'
  '修复|fix|解决'
  '删除|移除'
  '提交|commit|push'
  '跑测试|运行测试|build|npm|pnpm|yarn'
  '检查项目|分析代码'
  '修复测试失败'
  '用[[:space:]]*codex|用[[:space:]]*claude|帮我改代码|在当前项目中'
  '确保[[:space:]]*build[[:space:]]*通过|测试通过后提交|不要[[:space:]]*force[[:space:]]*push'
)
CONCEPT_PATTERNS=(
  '原理|概念|架构|理论|解释|是什么|为什么|教程|如何理解'
)

AUTO_ROUTING_ENABLED="$(read_auto_code_routing_flag)"
WORKING_DIR_IS_GIT_REPO="false"
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  WORKING_DIR_IS_GIT_REPO="true"
fi
GLOBAL_SKILL_EXISTS="false"
if [ -d "$HOME/.openclaw/skills/persistent-code-terminal" ]; then
  GLOBAL_SKILL_EXISTS="true"
fi

if [ "$AUTO_ROUTING_ENABLED" != "true" ]; then
  log_decision "false" "autoCodeRoutingDisabled" "$WORKING_DIR_IS_GIT_REPO"
  echo "Auto code routing is disabled (openclaw.config.dev.autoCodeRouting=false)."
  exit 2
fi

RAW_TEXT="$INPUT_TEXT"
INSTRUCTION="$RAW_TEXT"
TRIGGER_REASON=""

if printf '%s' "$RAW_TEXT" | grep -Eiq '不要执行，只分析'; then
  log_decision "false" "explicitBypass" "$WORKING_DIR_IS_GIT_REPO"
  echo "Bypass requested: analyze-only mode."
  exit 3
fi

if printf '%s' "$RAW_TEXT" | grep -Eiq '^[[:space:]]*(codex[[:space:]]+|用[[:space:]]*codex[[:space:]]+|使用[[:space:]]*codex[[:space:]]+)'; then
  INSTRUCTION="$(strip_prefix_instruction "$RAW_TEXT")"
  TRIGGER_REASON="explicitPrefix"
fi

HAS_ACTION="false"
if contains_any "$INSTRUCTION" "${ACTION_PATTERNS[@]}"; then
  HAS_ACTION="true"
fi

if [ -z "$TRIGGER_REASON" ] && [ "$HAS_ACTION" = "true" ]; then
  TRIGGER_REASON="matchedPattern"
fi

if [ -z "$TRIGGER_REASON" ]; then
  log_decision "false" "noActionVerb" "$WORKING_DIR_IS_GIT_REPO"
  echo "No coding workflow intent detected."
  exit 4
fi

if [ "$WORKING_DIR_IS_GIT_REPO" != "true" ] && [ "$GLOBAL_SKILL_EXISTS" != "true" ]; then
  log_decision "false" "notGitRepoAndNoGlobalSkill" "$WORKING_DIR_IS_GIT_REPO"
  echo "Current directory is not a git repository and no global skill install detected; auto execution skipped."
  exit 5
fi

if [ "$TRIGGER_REASON" = "matchedPattern" ] && contains_any "$INSTRUCTION" "${CONCEPT_PATTERNS[@]}" && [ "$HAS_ACTION" != "true" ]; then
  log_decision "false" "conceptualOnly" "$WORKING_DIR_IS_GIT_REPO"
  echo "Conceptual question detected; auto execution skipped."
  exit 6
fi

TASK_PROJECTS=()
TASK_INSTRUCTIONS=()
TASK_VALID=()
TASK_DIRS=()
PARSED_PROJECTS=()

while IFS= read -r segment || [ -n "$segment" ]; do
  segment="$(trim_spaces "$segment")"
  [ -z "$segment" ] && continue
  parsed="$(parse_segment "$segment" || true)"
  if [ -z "$parsed" ]; then
    continue
  fi

  project="${parsed%%|*}"
  instruction_part="${parsed#*|}"
  TASK_PROJECTS+=("$project")
  TASK_INSTRUCTIONS+=("$instruction_part")
  if is_known_project "$project"; then
    if dir="$(project_dir_from_name "$project" 2>/dev/null || true)"; then
      if [ -n "$dir" ] && [ -d "$dir" ]; then
        TASK_VALID+=("true")
        TASK_DIRS+=("$dir")
      else
        TASK_VALID+=("false")
        TASK_DIRS+=("")
      fi
    else
      TASK_VALID+=("false")
      TASK_DIRS+=("")
    fi
  else
    TASK_VALID+=("false")
    TASK_DIRS+=("")
  fi
  PARSED_PROJECTS+=("$project")
done < <(split_segments "$INSTRUCTION")

TASKS_COUNT="${#TASK_PROJECTS[@]}"
DETECTED_MULTI_PROJECT="false"
if [ "$TASKS_COUNT" -gt 1 ]; then
  DETECTED_MULTI_PROJECT="true"
fi
PARSED_PROJECTS_JOINED="$(printf '%s,' "${PARSED_PROJECTS[@]:-}" | sed 's/,$//')"

if [ "$PARSE_ONLY" -eq 1 ]; then
  log_decision "true" "parseOnly" "$WORKING_DIR_IS_GIT_REPO" "$DETECTED_MULTI_PROJECT" "$TASKS_COUNT" "$PARSED_PROJECTS_JOINED"
  if [ "$JSON_MODE" -eq 1 ]; then
    json='{"detectedMultiProject":'
    if [ "$DETECTED_MULTI_PROJECT" = "true" ]; then
      json+='true'
    else
      json+='false'
    fi
    json+=',"tasks":['
    first=1
    i=0
    while [ "$i" -lt "$TASKS_COUNT" ]; do
      [ "$first" -eq 0 ] && json+=','
      json+='{"project":"'"$(pct_json_escape "${TASK_PROJECTS[$i]}")"'","instruction":"'"$(pct_json_escape "${TASK_INSTRUCTIONS[$i]}")"'","valid":'"${TASK_VALID[$i]}"'}'
      first=0
      i=$((i + 1))
    done
    json+=']}'
    printf '%s\n' "$json"
  else
    echo "detectedMultiProject=$DETECTED_MULTI_PROJECT tasksCount=$TASKS_COUNT"
    i=0
    while [ "$i" -lt "$TASKS_COUNT" ]; do
      echo "${TASK_PROJECTS[$i]} | ${TASK_VALID[$i]} | ${TASK_INSTRUCTIONS[$i]}"
      i=$((i + 1))
    done
  fi
  exit 0
fi

if [ "$TASKS_COUNT" -gt 1 ]; then
  log_decision "true" "matchedMultiProjectPattern" "$WORKING_DIR_IS_GIT_REPO" "true" "$TASKS_COUNT" "$PARSED_PROJECTS_JOINED"
  echo "Multi-project execution report:"
  i=0
  while [ "$i" -lt "$TASKS_COUNT" ]; do
    project="${TASK_PROJECTS[$i]}"
    task_instruction="${TASK_INSTRUCTIONS[$i]}"
    valid="${TASK_VALID[$i]}"
    project_dir="${TASK_DIRS[$i]}"
    if [ "$valid" != "true" ]; then
      echo "- $project: failure (invalid project, not found)"
      i=$((i + 1))
      continue
    fi

    run_status=0
    auto_json=""
    summary_json=""
    if [ -z "$project_dir" ] || [ ! -d "$project_dir" ]; then
      echo "- $project: failure (cannot resolve project directory)"
      i=$((i + 1))
      continue
    fi

    if ! (cd "$project_dir" && "$BASE_DIR/persistent-code-terminal-start.sh" --project "$project" >/dev/null 2>&1); then
      echo "- $project: failure (cannot start session)"
      i=$((i + 1))
      continue
    fi
    if ! auto_json="$(cd "$project_dir" && "$BASE_DIR/persistent-code-terminal-auto.sh" --max-retries 3 --instruction "$task_instruction" --json 2>&1)"; then
      run_status=$?
    fi
    if ! summary_json="$(cd "$project_dir" && "$BASE_DIR/persistent-code-terminal-summary.sh" --lines 120 --json 2>/dev/null || true)"; then
      summary_json=""
    fi

    if [ "$run_status" -ne 0 ] && printf '%s' "$auto_json" | grep -Eiq 'tmux not found|codex not found'; then
      echo "- $project: failure (runtime missing). Run $BASE_DIR/persistent-code-terminal-doctor.sh"
      i=$((i + 1))
      continue
    fi

    success="$(json_get_scalar "$auto_json" "success")"
    attempts="$(json_get_scalar "$auto_json" "attempts")"
    final_exit="$(json_get_scalar "$auto_json" "finalExitCode")"
    err_line="$(json_get_first_error_line "$summary_json")"
    if [ -z "$err_line" ]; then
      err_line="-"
    fi
    if [ -z "$attempts" ]; then
      attempts="0"
    fi
    if [ -z "$final_exit" ]; then
      final_exit="null"
    fi
    if [ "$success" = "true" ]; then
      status_label="success"
    else
      status_label="failure"
    fi
    echo "- $project: $status_label (attempts=$attempts, exit=$final_exit, error=$err_line)"
    i=$((i + 1))
  done
  exit 0
fi

log_decision "true" "$TRIGGER_REASON" "$WORKING_DIR_IS_GIT_REPO" "false" "1" "$(basename "$(pwd)")"

AUTO_OUTPUT=""
AUTO_STATUS=0
if ! AUTO_OUTPUT="$("$BASE_DIR/persistent-code-terminal-auto.sh" --max-retries 3 --instruction "$INSTRUCTION" 2>&1)"; then
  AUTO_STATUS=$?
fi

if [ "$AUTO_STATUS" -ne 0 ] && printf '%s' "$AUTO_OUTPUT" | grep -Eiq 'tmux not found|codex not found'; then
  echo "Auto execution could not start ($AUTO_OUTPUT)."
  echo "Fallback: run $BASE_DIR/persistent-code-terminal-doctor.sh and retry."
  exit 7
fi

"$BASE_DIR/persistent-code-terminal-summary.sh" --lines 120


path: bin/persistent-code-terminal-send.sh
content: #!/usr/bin/env bash
set -euo pipefail
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=bin/persistent-code-terminal-lib.sh
source "$BASE_DIR/persistent-code-terminal-lib.sh"
SESSION="$(session_name)"
DRY_RUN=0
TIMEOUT=""
PHASE=""

usage() {
  cat <<'EOF_USAGE'
Usage:
  persistent-code-terminal-send.sh [--dry-run] [--timeout <seconds>] [--phase <name>] "<command>"
EOF_USAGE
}

while [ "$#" -gt 0 ]; do
  case "$1" in
    --dry-run)
      DRY_RUN=1
      shift
      ;;
    --timeout)
      if [ "$#" -lt 2 ]; then
        echo "--timeout requires a value in seconds." >&2
        exit 1
      fi
      TIMEOUT="$2"
      shift 2
      ;;
    --phase)
      if [ "$#" -lt 2 ]; then
        echo "--phase requires a value." >&2
        exit 1
      fi
      PHASE="$2"
      shift 2
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    --)
      shift
      break
      ;;
    -*)
      echo "Unknown option: $1" >&2
      usage >&2
      exit 1
      ;;
    *)
      break
      ;;
  esac
done

CMD="${1:-}"
if [ -z "$CMD" ]; then
  echo "No command provided." >&2
  usage >&2
  exit 1
fi

if [ -n "$TIMEOUT" ] && ! [[ "$TIMEOUT" =~ ^[0-9]+$ ]]; then
  echo "--timeout must be an integer number of seconds." >&2
  exit 1
fi

INNER_CMD="$CMD; printf '\\n__PCT_EXIT_CODE__%s\\n' \$?"
WRAPPED_ARGS=(bash -lc "$INNER_CMD")
WRAPPED_CMD=""
for arg in "${WRAPPED_ARGS[@]}"; do
  WRAPPED_CMD+="$(printf "%q " "$arg")"
done
WRAPPED_CMD="${WRAPPED_CMD% }"

if [ "$DRY_RUN" -eq 1 ]; then
  echo "$WRAPPED_CMD"
  exit 0
fi

ensure_session
pct_state_update_send "$CMD" "$PHASE"

BASE_SENTINEL_COUNT="$(tmux capture-pane -pt "$SESSION" -S -2000 | grep -c '__PCT_EXIT_CODE__' || true)"
tmux send-keys -t "$SESSION" "$WRAPPED_CMD" C-m

if [ -z "$TIMEOUT" ]; then
  exit 0
fi

START_TS=$SECONDS
while [ $((SECONDS - START_TS)) -lt "$TIMEOUT" ]; do
  READ_OUTPUT="$($BASE_DIR/persistent-code-terminal-read.sh)"
  CURRENT_SENTINEL_COUNT="$(printf '%s\n' "$READ_OUTPUT" | grep -c '__PCT_EXIT_CODE__' || true)"
  if [ "$CURRENT_SENTINEL_COUNT" -gt "$BASE_SENTINEL_COUNT" ] && printf '%s\n' "$READ_OUTPUT" | grep -Eq '__PCT_EXIT_CODE__[0-9]+'; then
    printf '%s\n' "$READ_OUTPUT"
    exit 0
  fi
  sleep 0.2
done

echo "Timed out waiting for command to finish"
echo "Tip: $BASE_DIR/persistent-code-terminal-attach.sh to watch progress."
exit 124


path: bin/persistent-code-terminal-session-name.sh
content: #!/usr/bin/env bash
set -euo pipefail
PROJECT_NAME=$(basename "$(pwd)" | tr -cs '[:alnum:]' '-')
echo "${PROJECT_NAME}-code-session"


path: bin/persistent-code-terminal-start.sh
content: #!/usr/bin/env bash
set -euo pipefail
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=bin/persistent-code-terminal-lib.sh
source "$BASE_DIR/persistent-code-terminal-lib.sh"
PROJECT_OVERRIDE=""

while [ "$#" -gt 0 ]; do
  case "$1" in
    --project)
      if [ "$#" -lt 2 ]; then
        echo "--project requires a value." >&2
        exit 1
      fi
      PROJECT_OVERRIDE="$2"
      shift 2
      ;;
    -h|--help)
      echo "Usage: persistent-code-terminal-start.sh [--project <custom-name>]"
      exit 0
      ;;
    *)
      echo "Unknown option: $1" >&2
      exit 1
      ;;
  esac
done

if [ -n "$PROJECT_OVERRIDE" ]; then
  SESSION="$(session_name_from_project "$PROJECT_OVERRIDE")"
else
  SESSION="$(session_name)"
fi

require_tmux
if tmux has-session -t "$SESSION" 2>/dev/null; then
  echo "Using existing session: $SESSION"
  pct_state_update_start "$SESSION"
  exit 0
fi

tmux new-session -d -s "$SESSION" -c "$(pwd)"
echo "Created session: $SESSION"
pct_state_update_start "$SESSION"


path: bin/persistent-code-terminal-status.sh
content: #!/usr/bin/env bash
set -euo pipefail
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=bin/persistent-code-terminal-lib.sh
source "$BASE_DIR/persistent-code-terminal-lib.sh"

require_tmux

PROJECT_DIR="$(pwd -P)"
SESSION="$(session_name)"
SESSION_STATUS="missing"
LAST_COMMAND=""
LAST_EXIT="unknown"

if has_session; then
  SESSION_STATUS="exists"
fi

if [ -f "$(pct_state_path)" ]; then
  LAST_COMMAND="$(pct_state_read_string "lastCommand")"
  LAST_EXIT_TOKEN="$(pct_state_read_token "lastExitCode" 2>/dev/null || true)"
  if [ -n "$LAST_EXIT_TOKEN" ] && [ "$LAST_EXIT_TOKEN" != "null" ]; then
    LAST_EXIT="$LAST_EXIT_TOKEN"
  fi
fi

if [ -z "$LAST_COMMAND" ]; then
  LAST_COMMAND="(none)"
fi

GIT_STATE="n/a"
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  if [ -n "$(git status --porcelain)" ]; then
    GIT_STATE="dirty"
  else
    GIT_STATE="clean"
  fi
fi

printf 'Project: %s\n' "$PROJECT_DIR"
printf 'Session: %s (%s)\n' "$SESSION" "$SESSION_STATUS"
printf 'Last command: %s\n' "$LAST_COMMAND"
printf 'Last exit code: %s\n' "$LAST_EXIT"
printf 'Git: %s\n' "$GIT_STATE"


path: bin/persistent-code-terminal-summary.sh
content: #!/usr/bin/env bash
set -euo pipefail
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=bin/persistent-code-terminal-lib.sh
source "$BASE_DIR/persistent-code-terminal-lib.sh"
SESSION="$(session_name)"
LINES=120
JSON_MODE=0

while [ "$#" -gt 0 ]; do
  case "$1" in
    --json)
      JSON_MODE=1
      shift
      ;;
    --lines)
      if [ "$#" -lt 2 ]; then
        echo "--lines requires a number." >&2
        exit 1
      fi
      LINES="$2"
      shift 2
      ;;
    -h|--help)
      echo "Usage: persistent-code-terminal-summary.sh [--lines <n>] [--json]"
      exit 0
      ;;
    *)
      echo "Unknown option: $1" >&2
      exit 1
      ;;
  esac
done

if ! [[ "$LINES" =~ ^[0-9]+$ ]]; then
  echo "--lines must be an integer." >&2
  exit 1
fi

if [ "$JSON_MODE" -eq 1 ]; then
  ensure_session >/dev/null
else
  ensure_session
fi

CAPTURED="$(tmux capture-pane -pt "$SESSION" -S "-$LINES")"
LATEST_EXIT="$(printf '%s\n' "$CAPTURED" | sed -n 's/.*__PCT_EXIT_CODE__\([0-9][0-9]*\).*/\1/p' | tail -n 1)"
pct_state_update_read_exit "$LATEST_EXIT"

PROJECT_DIR="$(pct_state_read_string "projectDir")"
STATE_SESSION="$(pct_state_read_string "session")"
LAST_COMMAND="$(pct_state_read_string "lastCommand")"
PHASE="$(pct_state_read_string "phase")"
UPDATED_AT="$(pct_state_read_string "updatedAt")"

if [ -z "$PROJECT_DIR" ]; then
  PROJECT_DIR="$(pwd -P)"
fi
if [ -z "$STATE_SESSION" ]; then
  STATE_SESSION="$SESSION"
fi
if [ -z "$UPDATED_AT" ]; then
  UPDATED_AT="$(pct_now_utc)"
fi

if [ -n "$LATEST_EXIT" ]; then
  LAST_EXIT_TOKEN="$LATEST_EXIT"
else
  LAST_EXIT_TOKEN="null"
fi

if [ "$JSON_MODE" -eq 1 ]; then
  ERRORS="$(printf '%s\n' "$CAPTURED" | grep -Ei 'error|failed|exception' || true)"
else
  ERRORS="$(printf '%s\n' "$CAPTURED" | grep -Ei 'error|Error:|FAILED|fail|exception|Traceback' || true)"
fi

if [ "$JSON_MODE" -eq 0 ]; then
  DISPLAY_EXIT="$LAST_EXIT_TOKEN"
  DISPLAY_COMMAND="$LAST_COMMAND"
  if [ -z "$DISPLAY_COMMAND" ]; then
    DISPLAY_COMMAND="(none)"
  fi
  if [ -z "$DISPLAY_EXIT" ] || [ "$DISPLAY_EXIT" = "null" ]; then
    DISPLAY_EXIT="unknown"
  fi

  printf 'Summary: exit=%s | cmd=%s\n' "$DISPLAY_EXIT" "$DISPLAY_COMMAND"
  printf '%s\n' '--- Recent Output ---'
  printf '%s\n' "$CAPTURED"

  if [ -n "$ERRORS" ]; then
    printf '%s\n' '--- Errors (grep) ---'
    printf '%s\n' "$ERRORS" | tail -n 40
  fi
  exit 0
fi

ERROR_LINES_JSON="[]"
if [ -n "$ERRORS" ]; then
  ERROR_LINES_JSON="["
  FIRST_LINE=1
  while IFS= read -r line; do
    if [ -z "$line" ]; then
      continue
    fi
    if [ "$FIRST_LINE" -eq 0 ]; then
      ERROR_LINES_JSON+=", "
    fi
    ERROR_LINES_JSON+="\"$(pct_json_escape "$line")\""
    FIRST_LINE=0
  done <<EOF_ERR_LINES
$ERRORS
EOF_ERR_LINES
  ERROR_LINES_JSON+="]"
fi

cat <<EOF_JSON
{
  "projectDir": "$(pct_json_escape "$PROJECT_DIR")",
  "session": "$(pct_json_escape "$STATE_SESSION")",
  "lastCommand": "$(pct_json_escape "$LAST_COMMAND")",
  "lastExitCode": ${LAST_EXIT_TOKEN},
  "phase": $(pct_json_quote_or_null "$PHASE"),
  "updatedAt": "$(pct_json_escape "$UPDATED_AT")",
  "recentOutput": "$(pct_json_escape "$CAPTURED")",
  "errorLines": ${ERROR_LINES_JSON}
}
EOF_JSON


path: bin/persistent-code-terminal-switch.sh
content: #!/usr/bin/env bash
set -euo pipefail
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=bin/persistent-code-terminal-lib.sh
source "$BASE_DIR/persistent-code-terminal-lib.sh"

PROJECT_NAME=""
SESSION_NAME_ARG=""

while [ "$#" -gt 0 ]; do
  case "$1" in
    --project)
      if [ "$#" -lt 2 ]; then
        echo "--project requires a value." >&2
        exit 1
      fi
      PROJECT_NAME="$2"
      shift 2
      ;;
    --session)
      if [ "$#" -lt 2 ]; then
        echo "--session requires a value." >&2
        exit 1
      fi
      SESSION_NAME_ARG="$2"
      shift 2
      ;;
    -h|--help)
      echo "Usage: persistent-code-terminal-switch.sh (--project <project-name> | --session <session-name>)"
      exit 0
      ;;
    *)
      echo "Unknown option: $1" >&2
      exit 1
      ;;
  esac
done

if [ -n "$PROJECT_NAME" ] && [ -n "$SESSION_NAME_ARG" ]; then
  echo "Use either --project or --session, not both." >&2
  exit 1
fi

if [ -z "$PROJECT_NAME" ] && [ -z "$SESSION_NAME_ARG" ]; then
  echo "One of --project or --session is required." >&2
  exit 1
fi

if [ -n "$PROJECT_NAME" ]; then
  TARGET_SESSION="$(session_name_from_project "$PROJECT_NAME")"
else
  TARGET_SESSION="$SESSION_NAME_ARG"
fi

require_tmux
if ! tmux has-session -t "$TARGET_SESSION" 2>/dev/null; then
  echo "Session not found: $TARGET_SESSION" >&2
  exit 1
fi

tmux attach -t "$TARGET_SESSION"


path: examples/case-study-patch-to-push.md
content: # Case Study: One sentence → Patch → Build/Test → Commit → Push

**Goal:** From a phone (SSH), ask Codex to add a small feature, verify, and push — while keeping full visibility and takeover ability.

## Setup

From repo root:

```bash
chmod +x skills/persistent-code-terminal/bin/*.sh
./skills/persistent-code-terminal/bin/persistent-code-terminal-start.sh
```

## One-sentence instruction to Codex (closed loop)

```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-codex-exec.sh "Add a /health endpoint that returns JSON {status:'ok'}. Add tests. Ensure build and tests pass. Commit with 'feat: add health endpoint' and push to current branch. Do NOT force push."
```

This uses the script defaults:
- `codex exec --full-auto --sandbox workspace-write --cd <current-dir> ...`

Optional (for automation logs):
```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-codex-exec.sh --json -o /tmp/codex-run.json "Add a /health endpoint that returns JSON {status:'ok'}. Add tests. Ensure build and tests pass. Commit with 'feat: add health endpoint' and push to current branch. Do NOT force push."
```

## Observe & intervene if needed

Attach to the session:

```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-attach.sh
```

If Codex gets stuck or fails tests, you can either:

1) **Give Codex a follow-up one-liner** (same repo, same session):
```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-codex-exec.sh "Fix the failing test output above with minimal changes. Rerun tests until green. Then push."
```

2) Or **take over manually** in the attached session (run build/test, edit files, etc.).

## Why this works well on mobile

- Your SSH connection can drop; the tmux session stays alive.
- Output is preserved; you can attach later and see the full history.
- You keep control: you can interrupt, inspect diffs, or stop before push.


path: examples/quickstart-manual.md
content: # Example: Patch → Build → Test → Commit → Push (manual commands)

This example is for users who prefer explicit steps (no Codex).

```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-start.sh
./skills/persistent-code-terminal/bin/persistent-code-terminal-send.sh "npm run build"
./skills/persistent-code-terminal/bin/persistent-code-terminal-read.sh
./skills/persistent-code-terminal/bin/persistent-code-terminal-send.sh "npm test"
./skills/persistent-code-terminal/bin/persistent-code-terminal-read.sh
./skills/persistent-code-terminal/bin/persistent-code-terminal-send.sh "git status"
./skills/persistent-code-terminal/bin/persistent-code-terminal-read.sh
./skills/persistent-code-terminal/bin/persistent-code-terminal-send.sh "git add -A"
./skills/persistent-code-terminal/bin/persistent-code-terminal-send.sh "git commit -m \"fix: make tests pass\""
./skills/persistent-code-terminal/bin/persistent-code-terminal-send.sh "git push"
./skills/persistent-code-terminal/bin/persistent-code-terminal-read.sh
```

Attach to watch:
```bash
./skills/persistent-code-terminal/bin/persistent-code-terminal-attach.sh
```


path: openclaw.config.dev.json
content: {
  "openclaw": {
    "config": {
      "dev": {
        "autoCodeRouting": false
      }
    }
  }
}


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