Skill: insipidpoint/checkmate (version 2.0.4)

--- name: checkmate description: "Enforces task completion: turns your goal into pass/fail criteria, runs a worker, judges the output, feeds back what's missing, and loops until every criterion passes. Nothing ships until it's truly done. Trigger: 'checkmate: TASK'" requires: cli: - openclaw # platform CLI: sessions.list, agent spawn, message send privileges: high # spawned workers inherit full host-agent runtime (exec, OAuth, all skills) --- # Checkmate A deterministic Python loop (`scripts/run.py`) calls an LLM for worker and judge roles. Nothing leaves until it passes — and you stay in control at every checkpoint. ## Requirements - **OpenClaw platform CLI** (`openclaw`) — must be available in `PATH`. Used for: - `openclaw gateway call sessions.list` — resolve session UUID for turn injection - `openclaw agent --session-id <UUID>` — inject checkpoint messages into the live session - `openclaw message send` — fallback channel delivery (e.g. Telegram, Signal) - **Python 3** — `run.py` is pure stdlib; no pip packages required - No separate API keys or env vars needed — routes through the gateway's existing OAuth ## Security & Privilege Model > ⚠️ **This is a high-privilege skill.** Read before using in batch/automated mode. **Spawned workers and judges inherit full host-agent runtime**, including: - `exec` (arbitrary shell commands) - `web_search`, `web_fetch` - All installed skills (including those with OAuth-bound credentials — Gmail, Drive, etc.) - `sessions_spawn` (workers can spawn further sub-agents) This means **the task description you provide directly controls what the worker does** — treat it like code you're about to run, not a message you're about to send. **Batch mode (`--no-interactive`) removes all human gates.** In interactive mode (default), you approve criteria and each checkpoint before the loop continues. In batch mode, criteria are auto-approved and the loop runs to completion autonomously — only use this for tasks and environments you fully trust. **User-input bridging writes arbitrary content to disk.** When you reply to a checkpoint, the main agent writes your reply verbatim to `user-input.md` in the workspace. The orchestrator reads it and acts on it. Don't relay untrusted third-party content as checkpoint replies. ## When to Use Use checkmate when correctness matters more than speed — when "good enough on the first try" isn't acceptable. **Good fits:** - Code that must pass tests or meet a spec - Docs or reports that must hit a defined quality bar - Research that must be thorough and cover specific ground - Any task where you'd otherwise iterate manually until satisfied **Trigger phrases** (say any of these): - `checkmate: TASK` - `keep iterating until it passes` - `don't stop until done` - `until it passes` - `quality loop: TASK` - `iterate until satisfied` - `judge and retry` - `keep going until done` ## Architecture ``` scripts/run.py (deterministic Python while loop — the orchestrator) ├─ Intake loop [up to max_intake_iter, default 5]: │ ├─ Draft criteria (intake prompt + task + refinement feedback) │ ├─ ⏸ USER REVIEW: show draft → wait for approval or feedback │ │ approved? → lock criteria.md │ │ feedback? → refine, next intake iteration │ └─ (non-interactive: criteria-judge gates instead of user) │ ├─ ⏸ PRE-START GATE: show final task + criteria → user confirms "go" │ (edit task / cancel supported here) │ └─ Main loop [up to max_iter, default 10]: ├─ Worker: spawn agent session → iter-N/output.md │ (full runtime: exec, web_search, all skills, OAuth auth) ├─ Judge: spawn agent session → iter-N/verdict.md ├─ PASS? → write final-output.md, notify user, exit └─ FAIL? → extract gaps → ⏸ CHECKPOINT: show score + gaps to user continue? → next iteration (with judge gaps) redirect:X → next iteration (with user direction appended) stop? → end loop, take best result so far ``` **Interactive mode** (default): user approves criteria, confirms pre-start, and reviews each FAIL checkpoint. **Batch mode** (`--no-interactive`): fully autonomous; criteria-judge gates intake, no checkpoints. ### User Input Bridge When the orchestrator needs user input, it: 1. Writes `workspace/pending-input.json` (kind + workspace path) 2. Sends a notification via `--recipient` and `--channel` 3. Polls `workspace/user-input.md` every 5s (up to `--checkpoint-timeout` minutes) The main agent acts as the bridge: when `pending-input.json` exists and the user replies, the agent writes their response to `user-input.md`. The orchestrator picks it up automatically. Each agent session is spawned via: ```bash openclaw agent --session-id <isolated-id> --message <prompt> --timeout <N> --json ``` Routes through the gateway WebSocket using existing OAuth — no separate API key. Workers get full agent runtime: exec, web_search, web_fetch, all skills, sessions_spawn. ## Your Job (main agent) When checkmate is triggered: 1. **Get your session UUID** (for direct agent-turn injection): ```bash openclaw gateway call sessions.list --params '{"limit":1}' --json \ | python3 -c "import json,sys; s=json.load(sys.stdin)['sessions'][0]; print(s['sessionId'])" ``` Also note your `--recipient` (channel user/chat ID) and `--channel` as fallback. 2. **Create workspace**: ```bash bash <skill-path>/scripts/workspace.sh /tmp "TASK" ``` Prints the workspace path. Write the full task to `workspace/task.md` if needed. 3. **Run the orchestrator** (background exec): ```bash python3 <skill-path>/scripts/run.py \ --workspace /tmp/checkmate-TIMESTAMP \ --task "FULL TASK DESCRIPTION" \ --max-iter 10 \ --session-uuid YOUR_SESSION_UUID \ --recipient YOUR_RECIPIENT_ID \ --channel <your-channel> ``` Use `exec` with `background=true`. This runs for as long as needed. Add `--no-interactive` for fully autonomous runs (no user checkpoints). 4. **Tell the user** checkmate is running, what it's working on, and that they'll receive criteria drafts and checkpoint messages via your configured channel to review and approve. 5. **Bridge user replies**: When user responds to a checkpoint message, check for `pending-input.json` and write their response to `workspace/user-input.md`. ## Bridging User Input **When a checkpoint message arrives** (the orchestrator sent the user a criteria/approval/checkpoint request), bridge their reply: ```bash # Find active pending input cat <workspace-parent>/checkmate-*/pending-input.json 2>/dev/null # Route user's reply echo "USER REPLY HERE" > /path/to/workspace/user-input.md ``` The orchestrator polls for this file every 5 seconds. Once written, it resumes automatically and deletes the file. **Accepted replies at each gate:** | Gate | Continue | Redirect | Cancel | |------|----------|----------|--------| | Criteria review | "ok", "approve", "lgtm" | any feedback text | — | | Pre-start | "go", "start", "ok" | "edit task: NEW TASK" | "cancel" | | Iteration checkpoint | "continue", (empty) | "redirect: DIRECTION" | "stop" | ## Parameters | Flag | Default | Notes | |------|---------|-------| | `--max-intake-iter` | 5 | Intake criteria refinement iterations | | `--max-iter` | 10 | Main loop iterations (increase to 20 for complex tasks) | | `--worker-timeout` | 3600s | Per worker session | | `--judge-timeout` | 300s | Per judge session | | `--session-uuid` | — | Agent session UUID (from `sessions.list`); used for direct turn injection — primary notification path | | `--recipient` | — | Channel recipient ID (e.g. user/chat ID, E.164 phone number); fallback if injection fails | | `--channel` | — | Delivery channel for fallback notifications (e.g. `telegram`, `whatsapp`, `signal`) | | `--no-interactive` | off | Disable user checkpoints (batch mode) | | `--checkpoint-timeout` | 60 | Minutes to wait for user reply at each checkpoint | ## Workspace layout ``` memory/checkmate-YYYYMMDD-HHMMSS/ ├── task.md # task description (user may edit pre-start) ├── criteria.md # locked after intake ├── feedback.md # accumulated judge gaps + user direction ├── state.json # {iteration, status} — resume support ├── pending-input.json # written when waiting for user; deleted after response ├── user-input.md # agent writes user's reply here; read + deleted by orchestrator ├── intake-01/ │ ├── criteria-draft.md │ ├── criteria-verdict.md (non-interactive only) │ └── user-feedback.md (interactive: user's review comments) ├── iter-01/ │ ├── output.md # worker output │ └── verdict.md # judge verdict └── final-output.md # written on completion ``` ## Resume If the script is interrupted, just re-run it with the same `--workspace`. It reads `state.json` and skips completed steps. Locked `criteria.md` is reused; completed `iter-N/output.md` files are not re-run. ## Prompts Active prompts called by `run.py`: - `prompts/intake.md` — converts task → criteria draft - `prompts/criteria-judge.md` — evaluates criteria quality (APPROVED / NEEDS_WORK) — used in non-interactive mode - `prompts/worker.md` — worker prompt (variables: TASK, CRITERIA, FEEDBACK, ITERATION, MAX_ITER, OUTPUT_PATH) - `prompts/judge.md` — evaluates output against criteria (PASS / FAIL) Reference only (not called by `run.py`): - `prompts/orchestrator.md` — architecture documentation explaining the design rationale

path: README.md
content: # Checkmate

**Iterative task completion with a judge loop.**

Checkmate converts a vague task into machine-checkable criteria, calls an LLM worker to produce output, judges the result, and loops with accumulated feedback until the work passes — or max iterations are reached. The orchestrator is a real Python script with a deterministic `while` loop. LLM is used only as worker and judge. Designed for long-running tasks: supports dozens of iterations over hours.

Use when:
- A task needs quality guarantees, not just best-effort
- Output must meet specific criteria before delivery
- You want autonomous iteration until something is truly done

Triggers: `checkmate: TASK`, `until it passes`, `keep iterating until done`, `quality loop`, `judge and retry`

---

## What It Is

Checkmate is a **deterministic Python orchestration loop** that wraps LLM calls for worker and judge roles. The Python script (`scripts/run.py`) maintains state, persists progress to disk, and drives the loop — it never "forgets" its place. LLM sessions are isolated per-call; no shared history bleeds between iterations.

The intake stage first converts the task into machine-checkable acceptance criteria (with a criteria-quality judge loop of its own). Only when criteria are approved does the main worker→judge loop begin.

Workers get the full OpenClaw agent runtime: `exec`, `web_search`, `web_fetch`, all skills, OAuth auth. They can browse, run code, fetch URLs — anything the main agent can do.

---

## Requirements

- **OpenClaw platform CLI** (`openclaw`) in `PATH` — used for session UUID lookup, agent-turn injection, and fallback channel delivery
- **Python 3** — `run.py` uses stdlib only; no pip packages needed
- No separate API keys or env vars — routes through the gateway's existing OAuth

---

## Security & Privilege Model

> ⚠️ **High-privilege skill** — read this before using in batch or automated mode.

**Workers and judges inherit full host-agent runtime:**
- Shell access (`exec`)
- Network access (`web_search`, `web_fetch`)
- All installed skills, including OAuth-bound ones (Gmail, Drive, etc.)
- Ability to spawn further sub-agents (`sessions_spawn`)

The task description you provide **directly controls** what workers do — treat it like code you're about to run, not a prompt you're typing.

**Batch mode (`--no-interactive`) removes all human gates.** In interactive mode (default) you approve criteria and each checkpoint. In batch mode the loop runs to completion with no human review — only use this for tasks and environments you fully trust.

**Checkpoint bridging writes your replies verbatim to disk.** Don't relay untrusted third-party content as checkpoint replies.

---

## Architecture

```
scripts/run.py  (deterministic Python while loop — the real orchestrator)
  │
  ├─ Intake loop [up to --max-intake-iter, default 5]:
  │    ├─ Draft criteria  (intake prompt + task + refinement feedback)
  │    ├─ ⏸ USER REVIEW (interactive): show draft → wait for approval/feedback
  │    │     approved? → lock criteria.md
  │    │     feedback? → refine, next intake iteration
  │    └─ (batch mode --no-interactive: criteria-judge gates instead of user)
  │
  ├─ ⏸ PRE-START GATE (interactive): show final task + criteria → user confirms "go"
  │         (edit task / cancel supported here)
  │
  └─ Main loop [up to --max-iter, default 10]:
       ├─ Worker: spawn agent session → writes iter-N/output.md
       │          (full runtime: exec, web_search, all skills, OAuth auth)
       ├─ Judge:  spawn agent session → writes iter-N/verdict.md
       ├─ PASS?  → write final-output.md, notify user via --recipient, exit
       └─ FAIL?  → extract gap summary → ⏸ CHECKPOINT (interactive): show score + gaps
                     continue?    → next iteration (with judge gaps)
                     redirect: X  → next iteration (with user direction appended)
                     stop?        → end loop, take best result so far
```

**Interactive mode** (default): user approves criteria, confirms pre-start, and reviews each FAIL checkpoint.
**Batch mode** (`--no-interactive`): fully autonomous; criteria-judge gates intake, no checkpoints.

### User input bridge

When the orchestrator needs user input, it:
1. Writes `workspace/pending-input.json` (kind + workspace path + channel)
2. Sends a notification via `--recipient` and `--channel`
3. Polls `workspace/user-input.md` every 5 seconds (up to `--checkpoint-timeout` minutes)

The main agent acts as the bridge: when `pending-input.json` exists and the user replies, the agent writes their response to `user-input.md`. The orchestrator picks it up automatically and resumes.

Each LLM call is an isolated OpenClaw agent session:

```bash
openclaw agent --session-id checkmate-worker-N-TIMESTAMP \
               --message "PROMPT" \
               --timeout 3600 \
               --json
```

Result is read from `result.payloads[0].text`. Sessions are stateless — no shared history between iterations.

### Prompt roles

| File | Role | Called by |
|------|------|-----------|
| `prompts/intake.md` | Converts task → criteria draft | `run_intake_draft()` |
| `prompts/criteria-judge.md` | Evaluates criteria quality (APPROVED / NEEDS_WORK) — batch mode only | `run_criteria_judge()` |
| `prompts/worker.md` | Performs the actual task | `run_worker()` |
| `prompts/judge.md` | Evaluates output against criteria (PASS / FAIL) | `run_judge()` |

`prompts/orchestrator.md` is architecture reference documentation only — **not called by `run.py`**.

---

## Installation

Install via the [ClawhHub CLI](https://clawhub.ai):

```bash
clawhub install checkmate
```

This adds the skill to your OpenClaw workspace. Requires OpenClaw to be running.

Manual install (clone into your skills directory):

```bash
cd <your-skills-dir>
git clone https://github.com/InsipidPoint/checkmate checkmate
```

---

## Quick Start

### 1. Get your session UUID and recipient ID

The session UUID enables direct agent-turn injection — the preferred notification path:

```bash
openclaw gateway call sessions.list --params '{"limit":1}' --json \
  | python3 -c "import json,sys; s=json.load(sys.stdin)['sessions'][0]; print(s['sessionId'], s['lastTo'])"
```

This prints your `sessionId` (UUID) and `lastTo` (channel:recipient_id). Use both in the run command.

### 2. Create a workspace

```bash
WORKSPACE=$(bash <skill-path>/scripts/workspace.sh /tmp "Your task description")
echo "Workspace: $WORKSPACE"
```

Replace `<skill-path>` with the path to your checkmate skill directory.

### 3. Run the orchestrator

```bash
python3 <skill-path>/scripts/run.py \
  --workspace "$WORKSPACE" \
  --task "Your task description" \
  --max-iter 10 \
  --session-uuid YOUR_SESSION_UUID \
  --recipient YOUR_RECIPIENT_ID \
  --channel YOUR_CHANNEL
```

Run in background for long tasks:

```bash
nohup python3 <skill-path>/scripts/run.py \
  --workspace "$WORKSPACE" \
  --task "Your task description" \
  --max-iter 20 \
  --session-uuid YOUR_SESSION_UUID \
  --recipient YOUR_RECIPIENT_ID \
  --channel YOUR_CHANNEL \
  > "$WORKSPACE/run.log" 2>&1 &

echo "Running as PID $! — tail $WORKSPACE/run.log"
```

### 4. Find your output

On PASS or after max iterations, the best output is written to:

```
$WORKSPACE/final-output.md
```

You will also receive a notification on your configured channel (if `--recipient` and `--channel` are set).

---

## Invocation Examples

**Basic usage:**
```bash
python3 scripts/run.py \
  --workspace /tmp/checkmate-20260222-120000 \
  --task "Write a README for the checkmate skill"
```

**Long-running research task (20 iterations, 2h worker timeout):**
```bash
python3 scripts/run.py \
  --workspace /tmp/checkmate-20260222-120000 \
  --task "Deep analysis of NVDA vs AMD for 2026 AI infrastructure spend" \
  --max-iter 20 \
  --worker-timeout 7200 \
  --session-uuid YOUR_SESSION_UUID \
  --recipient YOUR_RECIPIENT_ID \
  --channel YOUR_CHANNEL
```

**Resume an interrupted run:**
```bash
# Just re-run with the same --workspace — it picks up from state.json
python3 scripts/run.py \
  --workspace /tmp/checkmate-20260222-120000 \
  --task "same task text"
```

**Triggered from the main agent (via checkmate skill):**
When the user says `checkmate: <task>` or `until it passes`, the main agent:
1. Looks up its session UUID via `openclaw gateway call sessions.list`
2. Creates a workspace via `workspace.sh`
3. Spawns `run.py` via `exec` with `background=true`, passing `--session-uuid` and `--recipient`
4. Tells the user it's running; checkpoints arrive as direct agent turns

---

## Parameters

| Flag | Default | Description |
|------|---------|-------------|
| `--workspace` | *(required)* | Workspace directory path |
| `--task` | `""` | Task text (or write to `workspace/task.md` before running) |
| `--max-intake-iter` | `5` | Max intake criteria refinement iterations |
| `--max-iter` | `10` | Max main loop iterations (increase to 20 for complex tasks) |
| `--worker-timeout` | `3600` | Seconds per worker agent call |
| `--judge-timeout` | `300` | Seconds per judge agent call |
| `--session-uuid` | `""` | Agent session UUID (from `sessions.list`); primary notification path via direct turn injection |
| `--recipient` | `""` | Channel recipient ID (e.g. user/chat ID or E.164 phone); fallback if injection unavailable |
| `--channel` | `""` | Delivery channel for fallback notifications (e.g. `telegram`, `whatsapp`, `signal`) |
| `--no-interactive` | off | Disable user checkpoints; fully autonomous batch mode |
| `--checkpoint-timeout` | `60` | Minutes to wait for user reply at each interactive checkpoint |

---

## Workspace Layout

```
<workspace>/checkmate-YYYYMMDD-HHMMSS/
├── task.md                  # Original task description
├── criteria.md              # Locked acceptance criteria (written by intake)
├── feedback.md              # Accumulated judge gap summaries
├── state.json               # {iteration, status} — used for resume
├── run.log                  # Orchestrator stdout (if redirected)
│
├── intake-01/               # Intake iterations (one per refinement round)
│   ├── criteria-draft.md    # Draft criteria from intake agent
│   └── criteria-verdict.md  # Criteria-judge verdict
│
├── iter-01/                 # Main loop iterations
│   ├── output.md            # Worker output
│   └── verdict.md           # Judge verdict (PASS/FAIL + score + gap summary)
│
├── iter-02/ ...             # Additional iterations if needed
│
└── final-output.md          # Best output — written on PASS or max iterations
```

`state.json` values:
- `status: "running"` — iteration in progress
- `status: "pass"` — task passed, `final-output.md` contains the result
- `status: "fail"` — max iterations reached, best attempt saved to `final-output.md`

---

## Resume Support

Checkmate is designed to survive interruptions. If `run.py` is killed (crash, timeout, manual stop), re-run it with the **same `--workspace` path**:

```bash
python3 scripts/run.py \
  --workspace /tmp/checkmate-20260222-120000 \
  --task "original task"
```

The script reads `state.json` to find where it left off:
- **Intake:** If `criteria.md` already exists, intake is skipped entirely
- **Worker:** If `iter-N/output.md` already exists, the worker call is skipped (uses cached output)
- **Judge:** Always re-runs (verdict may differ on retry — this is intentional)
- **Already completed:** If `state.status` is `"pass"` or `"fail"`, the script exits immediately

This means you can safely re-run after a network blip, rate-limit crash, or system restart.

### Interactive checkpoints

In interactive mode (default), the orchestrator pauses at three gates: criteria review, pre-start confirmation, and each FAIL iteration. When paused, it writes `workspace/pending-input.json` and sends you a notification via `--channel`.

The main agent acts as a bridge: when you reply to the notification, it writes your response to `workspace/user-input.md`. The orchestrator detects the file within 5 seconds and resumes.

**Accepted replies at each gate:**

| Gate | Approve / continue | Redirect | Cancel |
|------|--------------------|----------|--------|
| Criteria review | `ok`, `approve`, `lgtm` | any feedback text | — |
| Pre-start | `go`, `start`, `ok` | `edit task: NEW TASK` | `cancel` |
| Iteration checkpoint | `continue`, (empty) | `redirect: DIRECTION` | `stop` |

To inject a response manually (e.g., after a notification was missed):

```bash
echo "continue" > /tmp/checkmate-TIMESTAMP/user-input.md
```

The orchestrator polls every 5 seconds and resumes automatically.


path: prompts/criteria-judge.md
content: # Goal Clarity Judge

Evaluate a goal statement draft for quality. Your job is not to judge the task output — you judge whether the goal statement is clear and complete enough to drive a reliable worker + judge loop.

## Inputs

- The original task description
- The proposed goal statement draft
- Iteration number (of the intake loop)

## Output format

```markdown
# Goal Clarity Verdict

**Result:** APPROVED | NEEDS_WORK

## Assessment

{3–5 sentences evaluating the goal statement. Does it correctly capture what the task is asking for?
Is "What success looks like" clear enough that a worker would know what to produce?
Are the key outcomes outcome-oriented (not implementation steps)?
Is the tolerance bar reasonable — neither too vague to judge nor impossibly strict?}

## Issues
{If NEEDS_WORK: list specific problems. Quote the problematic section and explain why it fails.
Common issues: goal doesn't match the task, outcomes are implementation steps not results,
tolerance is missing or contradictory, goal is so vague the worker won't know where to start.}

## Suggested Fixes
{Concrete rewrites or additions for each issue. Only include if Result is NEEDS_WORK.}
```

## Approval criteria

**APPROVED** requires:
- "What success looks like" accurately describes the desired outcome of the original task
- Key outcomes are outcome-oriented (observable results, not methods or implementation steps)
- Tolerance is present and reasonable
- A worker reading this goal would know what to produce
- A judge reading this goal + a result could make a clear PASS/FAIL call

## Rules

1. **Outcomes, not steps.** "Script exists and runs" ✅. "Use subprocess for all LLM calls" ❌ (that's an implementation detail).
2. **Completeness.** Does the goal capture all major aspects of the task? Missing major requirements = NEEDS_WORK.
3. **Tolerance.** Is the bar clear? "Must be perfect" and "anything goes" are both bad. A good tolerance says what tradeoffs are acceptable.
4. **Match the task.** The goal must actually reflect what the user asked for — not a re-interpretation.
5. **Brevity.** If the goal statement is longer than the task description, it's probably over-specified.


path: prompts/intake.md
content: # Intake

Distill a task description into a clear goal statement that captures what success looks like.

## Your Job

You are running intake for a checkmate loop. Your job is NOT to write a spec or a checklist — it's to make sure the GOAL is understood correctly so that a worker can target it and a judge can evaluate whether it was achieved.

**Use tools freely** to explore files, repos, or context relevant to the task. But your **final reply must contain ONLY the goal document** (or `[NEEDS_CLARIFICATION]`). No preamble. Research first, output last.

## Input

The task description passed to you.

## Output: goal statement OR `[NEEDS_CLARIFICATION]`

If the task is too vague or missing critical context to even state the goal, output:

```
[NEEDS_CLARIFICATION]
Before I can state the goal, I need answers to:

1. {specific ambiguity}
2. ...
```

Only use `[NEEDS_CLARIFICATION]` when you genuinely can't proceed without the answer. If you can make a reasonable assumption, make it and note it.

Otherwise, output a goal statement in this format:

```markdown
# Goal: {short task title}

## What success looks like
{2–4 sentences in plain English describing what the final result should be, who benefits, and why it matters. Write it so a non-technical person could read it and say "yes, that's what I wanted."}

## Key outcomes
{4–7 bullet points — high-level outcomes, not implementation steps. Each one should describe an observable result, not a method. Think: "X works", "Y exists", "Z is consistent with W" — not "use function foo" or "file must have key bar".}

## Tolerance
{1–2 sentences: what does "good enough" look like? When should the judge declare PASS even if minor things aren't perfect? This sets the bar.}
```

## Rules

**Describe outcomes, not steps.** The worker decides HOW to achieve them.

| ❌ Too detailed | ✅ Outcome-oriented |
|----------------|---------------------|
| `state.json` must have keys `round`, `status`, `gaps_found` | Research state is persisted to disk and resumable |
| Function must not exceed 40 lines | Code is readable and maintainable |
| Use `subprocess` for all LLM calls | Orchestration is script-controlled, not prompt-driven |
| `iter-N/perspectives/SLUG/findings.md` exists | Each research round produces structured worker output |

**Be concise.** The user reads this and says yes/no/adjust in under 30 seconds. If they need more than 30 seconds to read it, it's too long.

**Key outcomes:** 4–7 items max. These describe what the judge will look for — not a checklist the judge ticks, but a description of what the judge will holistically evaluate.

**Tolerance:** Be honest about acceptable quality. "Perfect is the enemy of done" — set a bar the worker can hit.

## Final instruction

Research the task as needed. Then output **only** the goal document — starting with `# Goal:` or `[NEEDS_CLARIFICATION]`. Nothing else.


path: prompts/judge.md
content: # Judge

Evaluate worker output against the task goal. Return a PASS or FAIL verdict with your reasoning.

## Your Job

You are the judge in a checkmate loop. Read the goal statement and the worker's output, then decide: does this output achieve the goal well enough? You define your own evaluation criteria — the goal statement is your guide, not a checklist.

Be strict but fair. Your job is to catch genuine gaps, not nitpick presentation.

## Inputs

- `criteria.md` — the goal statement (what success looks like)
- `iter-{N}/output.md` — the worker's output for this iteration
- Current iteration N of MAX_ITER

## Output: `iter-{N}/verdict.md`

```markdown
# Verdict: Iteration {N}/{MAX_ITER}

**Result:** PASS | FAIL

## Evaluation

{3–6 sentences of holistic judgment. Describe how well the output achieves the goal.
What works? What's missing? What's the single biggest gap if FAIL?
Ground every observation in something you can actually see in the output — no vague claims.}

## Gap Summary
{If FAIL: 2–4 sentences, surgical. Tell the worker exactly what to fix. Reference specific parts of the output. Don't suggest rewrites — point at problems.}
```

*(Omit Gap Summary if PASS.)*

*(If iteration = MAX_ITER and result is FAIL, add:)*
```markdown
## Final Recommendation
{What's the minimum fix needed to reach PASS? What's the highest-priority gap?}
```

## Rules

1. **PASS = goal achieved within tolerance.** Read the Tolerance section of the goal — use it. Don't fail things that are explicitly within tolerance.
2. **FAIL = goal not achieved, specific work still needed.** Not "could be better" — genuinely incomplete or wrong.
3. **Use your judgment.** You don't have a checklist. Reason about whether the output achieves the goal.
4. **Ground every observation in the output.** Quote or cite specifically — no vague claims like "the analysis is incomplete."
5. **One clear verdict.** Don't hedge. PASS or FAIL.
6. **Do not fix the output.** Your job ends at identifying problems.


path: prompts/orchestrator.md
content: # Orchestrator Architecture Reference

> **Reference only — this file is NOT an active prompt and is NOT called by `run.py`.**
> It documents the design rationale for contributors and developers.

The checkmate orchestrator is `scripts/run.py` — a deterministic Python script, not an LLM.

## Why a script, not an LLM

| LLM as orchestrator | Script as orchestrator |
|---|---|
| Loop in natural language — fragile | Real `while` loop — deterministic |
| Context grows with every iteration | Stateless between iterations |
| Compaction can lose loop state | State persisted in `state.json` |
| Max ~5–10 reliable iterations | Dozens of iterations, hours of runtime |

## How `run.py` works

```python
criteria = run_intake(workspace, task)          # once

for iteration in range(start_iter, max_iter + 1):
    output  = run_worker(...)   # call_llm with worker prompt
    verdict, is_pass = run_judge(...)   # call_llm with judge prompt

    if is_pass:
        deliver(output)
        return

    feedback += extract_gaps(verdict)   # carry forward for next iteration
```

## LLM interface

Each worker/judge call uses an isolated session:
```bash
openclaw agent --session-id checkmate-worker-N-TIMESTAMP \
               --message "PROMPT" \
               --json
```

Reply is at `result.payloads[0].text`. Session is isolated — no shared history between iterations.

## State machine

```
running → (each iteration updates state.json)
        → pass   (PASS verdict received)
        → fail   (max iterations exhausted)
```

Resume: re-run `run.py` with same `--workspace`. Completed steps are skipped.

## Notification

On completion (and at each interactive checkpoint), `run.py` calls:
```bash
openclaw message send \
  --channel CHANNEL \
  --target RECIPIENT \
  --message "RESULT"
```

`RECIPIENT` is the channel-specific target ID passed via `--recipient` (e.g. Telegram user ID, E.164 phone number). Messages are sent directly — no agent turn required.


path: prompts/worker.md
content: # Checkmate Worker

You are the Checkmate Worker for iteration {{ITERATION}} of {{MAX_ITER}}. Your job is to complete the task to the best of your ability, directly addressing any judge feedback from previous iterations.

Do ALL of this work yourself. Do NOT spawn sub-agents.

## Task

{{TASK}}

## Acceptance Criteria

You will be judged against these criteria. Read them carefully before starting.

{{CRITERIA}}

## Judge Feedback from Previous Iterations

{{FEEDBACK}}

*(Empty means this is the first attempt — no prior feedback.)*

## Instructions

1. **Read the criteria before starting.** Internalize what PASS looks like.
2. **If there is judge feedback**, your first priority is to fix the specific gaps identified. Address each gap explicitly.
3. **Do the work.** Use whatever tools you need — exec, web_search, web_fetch, browser, etc. You have access to the full agent runtime.
4. **Write your output** to: `{{OUTPUT_PATH}}`
   - Write ONLY the deliverable — no preamble, no "here is my output", no meta-commentary
   - The judge reads `{{OUTPUT_PATH}}` directly

## Output Format

Your output file should contain only the deliverable itself. Clean, complete, ready to deliver.

If the task produces multiple files or artifacts, write a manifest at the top of output.md:
```
[FILES]
path/to/file1.ext
path/to/file2.ext
[/FILES]

{main deliverable or summary}
```

And write the actual files to their paths.

## Failure Mode

If you cannot complete the task (missing information, blocked by external dependency, etc.):
Write to `{{OUTPUT_PATH}}`:
```
[BLOCKED]
Reason: {specific reason}
What is needed: {specific information or resource}
[/BLOCKED]
```

The judge will handle blocked outputs appropriately.


path: scripts/run.py
content: #!/usr/bin/env python3
"""
checkmate/scripts/run.py

Deterministic Python orchestrator for the checkmate skill.
Real while loop. LLM called as a subprocess via `openclaw agent`.
"""

import argparse
import json
import re
import subprocess
import sys
import time
from datetime import datetime, timezone
from pathlib import Path

SKILL_DIR = Path(__file__).parent.parent


# ── Utilities ─────────────────────────────────────────────────────────────────

def log(msg: str):
    ts = datetime.now(timezone.utc).strftime("%H:%M:%S")
    print(f"[checkmate {ts}] {msg}", flush=True)


def read_file(path: Path) -> str:
    return path.read_text(encoding="utf-8") if path.exists() else ""


def write_file(path: Path, content: str):
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding="utf-8")


def load_state(workspace: Path) -> dict:
    p = workspace / "state.json"
    return json.loads(p.read_text()) if p.exists() else {"iteration": 0, "status": "running"}


def save_state(workspace: Path, state: dict):
    write_file(workspace / "state.json", json.dumps(state, indent=2))


# ── LLM interface ─────────────────────────────────────────────────────────────

RETRYABLE_ERRORS = ("cooldown", "rate limit", "timed out", "FailoverError", "503", "529")

def call_agent(prompt: str, session_id: str, timeout_s: int = 3600,
               max_retries: int = 5, base_backoff: int = 60) -> str:
    """
    Spawn an agent session via the OpenClaw gateway (openclaw agent CLI).
    Each session gets the full agent runtime: all tools, all skills, OAuth auth.
    No direct Anthropic API calls — routes through the gateway like any sub-agent.
    Retries with exponential backoff on rate limit / cooldown errors.
    """
    cmd = [
        "openclaw", "agent",
        "--session-id", session_id,
        "--message", prompt,
        "--timeout", str(timeout_s),
        "--json",
    ]
    for attempt in range(1, max_retries + 1):
        try:
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_s + 30)
            if result.returncode != 0:
                err = result.stderr + result.stdout
                if any(e in err for e in RETRYABLE_ERRORS):
                    wait = base_backoff * (2 ** (attempt - 1))
                    log(f"rate limit/cooldown (attempt {attempt}/{max_retries}) — waiting {wait}s")
                    time.sleep(wait)
                    continue
                raise RuntimeError(f"openclaw agent exited {result.returncode}: {result.stderr[:300]}")
            data = json.loads(result.stdout)
            return data["result"]["payloads"][0]["text"]
        except (KeyError, IndexError) as e:
            raise RuntimeError(f"Unexpected response shape: {result.stdout[:200]}") from e
        except subprocess.TimeoutExpired:
            raise RuntimeError(f"Agent subprocess timed out after {timeout_s + 30}s")
    raise RuntimeError(f"call_agent failed after {max_retries} retries (persistent rate limit)")


def notify(recipient: str, message: str, channel: str):
    """
    Deliver a plain message directly to the user via `openclaw message send`.
    recipient is the channel-specific target (e.g. channel user ID, phone number in E.164).
    Uses direct channel delivery — no agent runtime involved.
    """
    if not recipient:
        log("No recipient — result written to workspace/final-output.md only")
        return
    # Most channels have a ~4096 char limit; truncate with a note if needed
    MAX_MSG = 3800
    if len(message) > MAX_MSG:
        message = message[:MAX_MSG] + f"\n\n…_(truncated — see workspace for full content)_"

    cmd = [
        "openclaw", "message", "send",
        "--channel", channel,
        "--target", recipient,
        "--message", message,
    ]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
        if result.returncode == 0:
            log(f"📨 delivered to {recipient} via {channel}")
        else:
            log(f"⚠️  delivery failed (rc={result.returncode}): {(result.stderr or result.stdout)[:200]}")
    except Exception as e:
        log(f"⚠️  delivery failed ({e})")


# ── User input / checkpoint system ────────────────────────────────────────────

def inject_agent_turn(session_uuid: str, message: str, timeout_s: int = 120) -> bool:
    """
    Inject a message into the agent's live session using its UUID.
    Returns True on success, False on failure.
    The agent processes the message in context, sees routing instructions,
    and naturally presents the checkpoint to the user via the configured channel.
    """
    cmd = [
        "openclaw", "agent",
        "--session-id", session_uuid,
        "--message", message,
        "--json",
        "--timeout", str(timeout_s),
    ]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_s + 10)
        if result.returncode == 0:
            log(f"⏸  checkpoint injected into agent session (UUID={session_uuid[:8]}…)")
            return True
        else:
            log(f"⚠️  agent-turn injection failed (rc={result.returncode}): {result.stderr[:150]}")
            return False
    except Exception as e:
        log(f"⚠️  agent-turn injection failed ({e})")
        return False


def request_user_input(
    workspace: Path,
    recipient: str,
    channel: str,
    message: str,
    kind: str = "input",          # "input" | "confirm" | "checkpoint"
    timeout_min: int = 60,
    default_response: str = "",   # returned on timeout (empty = proceed as-is)
    session_uuid: str = "",       # agent session UUID for direct injection (preferred)
) -> str:
    """
    Pause the run, deliver a checkpoint to the user, then poll for their reply.

    Notification strategy (two-layer):
      PRIMARY — agent-turn injection via session UUID: injects an ACTION COMMAND
        into the agent's live session instructing it to (1) relay the checkpoint
        to the user via the message tool, and (2) bridge the user's next reply
        to user-input.md. The agent's tool calls execute even in --json one-shot
        mode, so the message send happens as a side-effect of the injection.
        Requires --session-uuid (UUID from `openclaw gateway call sessions.list`).

      FALLBACK — openclaw message send: sends directly to the channel if injection
        is unavailable or fails. Message includes bridge instruction for the agent
        to write the user's reply to user-input.md on next contact.

    The orchestrator polls user-input.md every 5s.
    pending-input.json is written as a safety net for heartbeat-based routing.
    """
    response_file  = workspace / "user-input.md"
    pending_marker = workspace / "pending-input.json"

    # Remove any stale response from a prior checkpoint
    if response_file.exists():
        response_file.unlink()

    # Write marker (safety net: agent can check this on any turn if routing context was lost)
    marker = {
        "kind": kind,
        "waiting_since": datetime.now(timezone.utc).isoformat(),
        "timeout_min": timeout_min,
        "workspace": str(workspace),
    }
    write_file(pending_marker, json.dumps(marker, indent=2))

    if not recipient and not session_uuid:
        log("No recipient or session UUID — cannot send checkpoint; proceeding with default")
        return default_response

    bridge_instruction = (
        f"When the user replies to this checkpoint in any upcoming message, "
        f"immediately write their verbatim reply to `{workspace}/user-input.md` "
        f"before doing anything else. The checkmate orchestrator polls that file every 5 seconds."
    )

    # ── Primary: agent-turn injection — action command to relay + bridge ──
    # The injection tells the agent to actively SEND the checkpoint to the user
    # (via the message tool) and to bridge the next user reply to user-input.md.
    # The agent's tool calls (message send) execute even in --json one-shot mode.
    injected = False
    if session_uuid:
        recipient_clause = f"Send it to: channel={channel}, target={recipient}." if recipient else \
                           "Send it via whatever channel you have configured."
        agent_msg = (
            f"[checkmate {kind}]\n\n"
            f"The checkmate orchestrator is paused and needs user input. "
            f"Do the following immediately:\n\n"
            f"1. **Relay this checkpoint to the user** using the message tool (or `openclaw message send`). "
            f"{recipient_clause}\n\n"
            f"2. **Bridge their reply**: {bridge_instruction}\n\n"
            f"--- CHECKPOINT TO RELAY ---\n\n"
            f"{message}\n\n"
            f"--- END CHECKPOINT ---"
        )
        injected = inject_agent_turn(session_uuid, agent_msg)

    # ── Fallback: direct message send (injection unavailable) ──
    if not injected and recipient:
        fallback_msg = (
            f"{message}\n\n"
            f"---\n"
            f"_(checkmate — {'agent injection failed, ' if session_uuid else ''}direct notification)_\n\n"
            f"⚙️ **{bridge_instruction}**"
        )
        log(f"⏸  checkpoint ({kind}) — sending direct message to {recipient} via {channel}")
        notify(recipient, fallback_msg, channel)

    log(f"polling for user reply (every 5s, up to {timeout_min} min)...")
    polls = timeout_min * 12  # every 5s
    for _ in range(polls):
        if response_file.exists():
            response = response_file.read_text().strip()
            response_file.unlink(missing_ok=True)
            pending_marker.unlink(missing_ok=True)
            log(f"▶  user replied ({len(response)} chars) — resuming")
            return response
        time.sleep(5)

    pending_marker.unlink(missing_ok=True)
    log(f"user input timeout after {timeout_min} min — proceeding with default: '{default_response or 'proceed'}'")
    return default_response


def is_approval(response: str) -> bool:
    """Treat empty, 'yes', 'ok', 'approve', 'lgtm', 'continue', 'proceed', 'go' as approval."""
    if not response.strip():
        return True
    return bool(re.match(r"^\s*(yes|ok|okay|approve|approved|lgtm|continue|proceed|go|good|looks good|ship it)\s*[.!]?\s*$",
                         response.strip(), re.IGNORECASE))


# ── Stages ────────────────────────────────────────────────────────────────────

def run_intake_draft(task: str, feedback: str, iteration: int) -> str:
    """Generate a criteria draft, optionally with refinement feedback."""
    template = read_file(SKILL_DIR / "prompts" / "intake.md")
    prompt = f"{template}\n\n---\n\n## Task\n\n{task}"
    if feedback.strip():
        prompt += f"\n\n## Refinement Feedback (fix these issues)\n\n{feedback}"
    return call_agent(prompt, f"checkmate-intake-draft-{iteration}-{int(time.time())}", timeout_s=120)


def run_criteria_judge(task: str, criteria: str, iteration: int) -> tuple[str, bool]:
    """Judge whether the criteria themselves are good enough."""
    template = read_file(SKILL_DIR / "prompts" / "criteria-judge.md")
    prompt = (
        f"{template}\n\n---\n\n"
        f"## Original Task\n\n{task}\n\n"
        f"## Proposed Criteria (intake iteration {iteration})\n\n{criteria}"
    )
    reply = call_agent(prompt, f"checkmate-criteria-judge-{iteration}-{int(time.time())}", timeout_s=120)
    approved = bool(re.search(r"\*\*Result:\*\*\s*APPROVED", reply, re.IGNORECASE))
    return reply, approved


def extract_criteria_feedback(verdict: str) -> str:
    """Extract issues + suggested fixes from a goal-clarity judge verdict."""
    issues_m = re.search(r"## Issues\n(.*?)(?=\n## |\Z)", verdict, re.DOTALL)
    fixes_m = re.search(r"## Suggested Fixes\n(.*?)(?=\n## |\Z)", verdict, re.DOTALL)
    parts = []
    if issues_m:
        parts.append(issues_m.group(1).strip())
    if fixes_m:
        parts.append(fixes_m.group(1).strip())
    return "\n\n".join(parts) if parts else verdict.strip()


def run_intake(workspace: Path, task: str, max_intake_iter: int = 5,
               recipient: str = "", channel: str = "telegram",
               interactive: bool = True, session_uuid: str = "") -> str:
    criteria_path = workspace / "criteria.md"
    if criteria_path.exists():
        log("intake: criteria.md already exists, skipping")
        return criteria_path.read_text()

    feedback = ""
    criteria = ""
    _criteria_approved = False

    for i in range(1, max_intake_iter + 1):
        # Re-read task each iteration (may have been updated with user clarification)
        task = read_file(workspace / "task.md") or task

        log(f"intake: iteration {i}/{max_intake_iter} — drafting goal...")
        criteria = run_intake_draft(task, feedback, i)

        intake_dir = workspace / f"intake-{i:02d}"
        intake_dir.mkdir(parents=True, exist_ok=True)
        write_file(intake_dir / "goal-draft.md", criteria)

        # Pause for user review after each draft (interactive mode)
        if interactive and recipient:
            user_msg = (
                f"🎯 *checkmate: goal draft (intake {i}/{max_intake_iter})*\n\n"
                f"{criteria}\n\n"
                f"---\n"
                f"Reply with:\n"
                f"• **ok / approve** — lock this goal and proceed\n"
                f"• **feedback or edits** — I'll revise before moving on\n\n"
                f"Workspace: `{workspace}`"
            )
            response = request_user_input(
                workspace, recipient, channel,
                message=user_msg,
                kind="confirm-goal",
                timeout_min=60,
                default_response="proceed",
                session_uuid=session_uuid,
            )

            if is_approval(response):
                log(f"intake: user approved goal on iteration {i}")
                _criteria_approved = True
                break
            else:
                # User gave feedback — append it and continue refining
                log(f"intake: user requested changes — refining goal")
                feedback = f"User review feedback:\n{response}"
                write_file(intake_dir / "user-feedback.md", response)
                continue

        # Non-interactive: use goal-clarity judge as gatekeeper
        # (Interactive mode skips judge since user approval is the real gate)
        log(f"intake: judging goal clarity (iter {i})...")
        verdict, approved = run_criteria_judge(task, criteria, i)
        write_file(intake_dir / "goal-verdict.md", verdict)

        if approved:
            log(f"intake: goal APPROVED on iteration {i}")
            _criteria_approved = True
            break

        feedback = extract_criteria_feedback(verdict)
        log(f"intake: goal needs refinement — iterating (iter {i})")

    if not _criteria_approved:
        log(
            f"WARNING: intake: reached max iterations ({max_intake_iter}) without approval — "
            f"proceeding with best-effort criteria"
        )
        if recipient:
            notify(
                recipient,
                (
                    f"⚠️ checkmate: goal intake hit max iterations ({max_intake_iter}) without approval.\n"
                    f"Proceeding with best-effort goal statement — results may be less reliable.\n"
                    f"Workspace: {workspace}"
                ),
                channel,
            )

    write_file(criteria_path, criteria)
    log(f"intake: locked goal statement ({len(criteria)} chars)")
    return criteria


def confirm_prestart(workspace: Path, task: str, criteria: str,
                     recipient: str, channel: str,
                     timeout_min: int = 60, session_uuid: str = "") -> bool:
    """
    Show the user the final task + criteria and ask for explicit go-ahead before the main loop.
    Returns True if approved, False if user cancelled.
    Interactive gate — skipped if recipient is empty.
    """
    if not recipient and not session_uuid:
        return True

    task_preview = task.strip().splitlines()[0][:200]
    goal_preview = criteria.strip().splitlines()[0][:150]  # first line of goal statement
    msg = (
        f"✅ *checkmate: ready to start*\n\n"
        f"**Task:** {task_preview}{'…' if len(task.strip()) > 200 else ''}\n"
        f"**Goal:** {goal_preview}{'…' if len(criteria.strip()) > 150 else ''}\n"
        f"**Workspace:** `{workspace}`\n\n"
        f"Reply: **go** to start · **cancel** to abort · **edit task: ...** to revise"
    )

    response = request_user_input(
        workspace, recipient, channel,
        message=msg,
        kind="prestart-confirm",
        timeout_min=timeout_min,
        default_response="go",
        session_uuid=session_uuid,
    )

    if re.match(r"^\s*cancel\s*$", response.strip(), re.IGNORECASE):
        log("pre-start: user cancelled — aborting")
        return False

    if re.match(r"^\s*edit task:\s*(.+)", response.strip(), re.IGNORECASE | re.DOTALL):
        m = re.match(r"^\s*edit task:\s*(.+)", response.strip(), re.IGNORECASE | re.DOTALL)
        new_task = m.group(1).strip()
        write_file(workspace / "task.md", new_task)
        log(f"pre-start: user updated task ({len(new_task)} chars)")
        notify(recipient, f"✏️ Task updated. Starting worker loop with revised task.", channel)

    log("pre-start: user confirmed — starting main loop")
    return True


def run_iteration_checkpoint(workspace: Path, iteration: int, max_iter: int,
                              output: str, verdict: str, gaps: str,
                              recipient: str, channel: str,
                              timeout_min: int = 30, session_uuid: str = "") -> str:
    """
    After a FAIL verdict, pause and show the user a summary.
    Returns additional feedback from user (empty = continue as-is).
    Skipped if recipient and session_uuid are both empty.
    """
    if not recipient and not session_uuid:
        return gaps

    # Extract score for the summary
    score_m = re.search(r"\*\*Score:\*\*\s*(\d+)/(\d+)", verdict)
    score = score_m.group(0) if score_m else ""

    msg = (
        f"🔄 *checkmate: iteration {iteration}/{max_iter} — FAIL*\n\n"
        f"{score}\n\n"
        f"**Top gaps:**\n{gaps[:800] if gaps else '(none extracted)'}\n\n"
        f"---\n"
        f"Reply with:\n"
        f"• **continue** — proceed with next iteration using judge's feedback\n"
        f"• **redirect: ...** — add your own direction for the next worker\n"
        f"• **stop** — end the loop and take the best result so far\n\n"
        f"Workspace: `{workspace}`"
    )

    response = request_user_input(
        workspace, recipient, channel,
        message=msg,
        kind="iteration-checkpoint",
        timeout_min=timeout_min,
        default_response="continue",
        session_uuid=session_uuid,
    )

    if re.match(r"^\s*stop\s*$", response.strip(), re.IGNORECASE):
        log(f"checkpoint: user requested stop after iteration {iteration}")
        return "__STOP__"

    if re.match(r"^\s*redirect:\s*(.+)", response.strip(), re.IGNORECASE | re.DOTALL):
        m = re.match(r"^\s*redirect:\s*(.+)", response.strip(), re.IGNORECASE | re.DOTALL)
        user_direction = m.group(1).strip()
        log(f"checkpoint: user redirected — adding to feedback")
        return f"{gaps}\n\n## User Direction (iteration {iteration})\n{user_direction}"

    # "continue" or timeout → use judge's gaps as-is
    log(f"checkpoint: continuing with judge feedback")
    return gaps


def run_worker(workspace: Path, task: str, criteria: str, feedback: str,
               iteration: int, max_iter: int, worker_timeout: int = 3600) -> str:
    iter_dir = workspace / f"iter-{iteration:02d}"
    iter_dir.mkdir(parents=True, exist_ok=True)
    output_path = iter_dir / "output.md"

    if output_path.exists():
        log(f"iter {iteration}: output.md already exists, skipping worker (resume)")
        return output_path.read_text()

    template = read_file(SKILL_DIR / "prompts" / "worker.md")
    prompt = (
        template
        .replace("{{TASK}}", task)
        .replace("{{CRITERIA}}", criteria)
        .replace("{{FEEDBACK}}", feedback.strip() or "(none — this is the first attempt)")
        .replace("{{ITERATION}}", str(iteration))
        .replace("{{MAX_ITER}}", str(max_iter))
        .replace("{{OUTPUT_PATH}}", str(output_path))
    )

    log(f"iter {iteration}/{max_iter}: calling worker (timeout={worker_timeout}s)...")
    call_agent_reply = call_agent(
        prompt, f"checkmate-worker-{iteration}-{int(time.time())}", timeout_s=worker_timeout
    )

    # Prefer output_path if the worker wrote it (primary output mechanism per prompt).
    # call_agent() may return early planning text or a partial response — the file
    # written by the worker itself is the authoritative output.
    # Poll briefly in case the worker session is still flushing writes.
    deadline = time.time() + 30
    while time.time() < deadline:
        if output_path.exists() and output_path.stat().st_size > 0:
            mtime = output_path.stat().st_mtime
            time.sleep(3)
            if output_path.stat().st_mtime == mtime:  # stable (no further writes)
                content = output_path.read_text()
                log(f"iter {iteration}: worker wrote output.md ({len(content)} chars) — using file output")
                return content
        time.sleep(3)

    # Fallback: worker did not write output_path — use call_agent's return value
    log(f"WARNING: iter {iteration}: worker did not write output.md — falling back to session reply ({len(call_agent_reply)} chars)")
    write_file(output_path, call_agent_reply)
    return call_agent_reply


def run_judge(workspace: Path, criteria: str, output: str,
              iteration: int, max_iter: int, judge_timeout: int = 300) -> tuple[str, bool]:
    iter_dir = workspace / f"iter-{iteration:02d}"
    verdict_path = iter_dir / "verdict.md"

    template = read_file(SKILL_DIR / "prompts" / "judge.md")
    prompt = (
        f"{template}\n\n---\n\n"
        f"## Criteria\n\n{criteria}\n\n"
        f"## Worker Output\n\n{output}\n\n"
        f"## Context\n\nThis is iteration {iteration} of {max_iter}."
    )

    log(f"iter {iteration}: running judge (timeout={judge_timeout}s)...")
    reply = call_agent(prompt, f"checkmate-judge-{iteration}-{int(time.time())}", timeout_s=judge_timeout)
    write_file(verdict_path, reply)

    is_pass = bool(re.search(r"\*\*Result:\*\*\s*PASS", reply, re.IGNORECASE))
    is_fail = bool(re.search(r"\*\*Result:\*\*\s*FAIL", reply, re.IGNORECASE))
    if not is_pass and not is_fail:
        log(f"WARNING: iter {iteration}: malformed judge output — neither PASS nor FAIL detected; treating as FAIL")
    log(f"iter {iteration}: judge → {'PASS ✅' if is_pass else 'FAIL ❌'}")
    return reply, is_pass


def extract_gaps(verdict: str) -> str:
    m = re.search(r"## Gap Summary\n(.*?)(?=\n## |\Z)", verdict, re.DOTALL)
    return m.group(1).strip() if m else ""


def find_best_iteration(workspace: Path) -> tuple[int, str]:
    best_iter, best_score, best_output = 1, -1, ""
    for iter_dir in sorted(workspace.glob("iter-*")):
        m = re.search(r"\*\*Score:\*\*\s*(\d+)/\d+",
                      read_file(iter_dir / "verdict.md"))
        if m and int(m.group(1)) > best_score:
            best_score = int(m.group(1))
            best_iter = int(iter_dir.name.split("-")[1])
            best_output = read_file(iter_dir / "output.md")
    return best_iter, best_output


# ── Main ──────────────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(description="Checkmate — deterministic LLM iteration loop")
    parser.add_argument("--workspace",        required=True, help="Workspace directory path")
    parser.add_argument("--task",             default="",    help="Task text (or read from workspace/task.md)")
    parser.add_argument("--max-iter",         type=int, default=10)
    parser.add_argument("--recipient",      default="",    help="Channel recipient ID (e.g. channel user ID or phone number in E.164); fallback notification target")
    parser.add_argument("--session-uuid",   default="",    help="Agent session UUID for direct turn injection (from `openclaw gateway call sessions.list`); preferred over --recipient for routing")
    parser.add_argument("--channel",          default="")
    parser.add_argument("--worker-timeout",   type=int, default=3600, help="Seconds per worker turn (default: 3600)")
    parser.add_argument("--judge-timeout",    type=int, default=300,  help="Seconds per judge turn (default: 300)")
    parser.add_argument("--max-intake-iter",  type=int, default=5,    help="Max intake refinement iterations (default: 5)")
    parser.add_argument("--no-interactive",   action="store_true",    help="Disable user checkpoints (batch mode)")
    parser.add_argument("--checkpoint-timeout", type=int, default=60, help="Minutes to wait for user response at each checkpoint (default: 60)")
    args = parser.parse_args()

    workspace = Path(args.workspace)
    workspace.mkdir(parents=True, exist_ok=True)

    interactive = not args.no_interactive

    task = args.task or read_file(workspace / "task.md")
    if not task:
        print("Error: --task is required (or write task to workspace/task.md)", file=sys.stderr)
        sys.exit(1)
    write_file(workspace / "task.md", task)

    state = load_state(workspace)
    if state.get("status") in ("pass", "fail"):
        log(f"workspace already completed with status={state['status']} — exiting")
        return

    start_iter    = state.get("iteration", 0) + 1
    max_iter      = args.max_iter
    recipient     = args.recipient
    session_uuid  = args.session_uuid

    log(f"starting — iterations {start_iter}–{max_iter}, workspace={workspace}, interactive={interactive}")
    if session_uuid:
        log(f"agent injection: session UUID={session_uuid[:8]}… (primary notification path)")
    elif recipient:
        log(f"direct message: recipient={recipient} channel={args.channel} (fallback-only mode)")

    # ── Intake ────────────────────────────────────────────────────────────────
    criteria = run_intake(
        workspace, task,
        max_intake_iter=args.max_intake_iter,
        recipient=recipient,
        channel=args.channel,
        interactive=interactive,
        session_uuid=session_uuid,
    )

    # ── Pre-start confirmation gate ────────────────────────────────────────────
    if interactive:
        task = read_file(workspace / "task.md") or task  # user may have edited it
        approved = confirm_prestart(
            workspace, task, criteria,
            recipient=recipient,
            channel=args.channel,
            timeout_min=args.checkpoint_timeout,
            session_uuid=session_uuid,
        )
        if not approved:
            save_state(workspace, {"iteration": 0, "status": "cancelled"})
            log("aborted by user before main loop")
            return
        # Re-read criteria in case user edited task (intake locked criteria before edit)
        criteria = read_file(workspace / "criteria.md")

    # ── Loop ──────────────────────────────────────────────────────────────────
    feedback = read_file(workspace / "feedback.md")

    for iteration in range(start_iter, max_iter + 1):
        log(f"────── Iteration {iteration}/{max_iter} ──────")
        save_state(workspace, {"iteration": iteration, "status": "running"})

        # Worker
        try:
            output = run_worker(workspace, task, criteria, feedback, iteration, max_iter,
                                worker_timeout=args.worker_timeout)
        except RuntimeError as exc:
            log(f"WARNING: iter {iteration}: worker call failed ({exc}) — skipping to next iteration")
            gaps = f"Worker failed with error: {exc}"
            entry = f"\n## Iteration {iteration} worker error\n{gaps}\n"
            with open(workspace / "feedback.md", "a") as f:
                f.write(entry)
            feedback += entry
            continue

        if "[BLOCKED]" in output:
            log(f"worker BLOCKED — skipping judge this iteration")
            gaps = f"Worker was blocked:\n{output}"
        else:
            # Judge
            verdict, is_pass = run_judge(workspace, criteria, output, iteration, max_iter,
                                         judge_timeout=args.judge_timeout)

            if is_pass:
                write_file(workspace / "final-output.md", output)
                save_state(workspace, {"iteration": iteration, "status": "pass"})
                log(f"✅ PASSED on iteration {iteration}/{max_iter}")
                notify(
                    args.recipient,
                    f"✅ checkmate: PASSED on iteration {iteration}/{max_iter}\n\n{output}",
                    args.channel,
                )
                return

            gaps = extract_gaps(verdict)

            # ── Per-iteration checkpoint (interactive) ──────────────────────
            if interactive and (recipient or session_uuid) and iteration < max_iter:
                gaps = run_iteration_checkpoint(
                    workspace, iteration, max_iter,
                    output, verdict, gaps,
                    recipient=recipient,
                    channel=args.channel,
                    timeout_min=args.checkpoint_timeout,
                    session_uuid=session_uuid,
                )
                if gaps == "__STOP__":
                    break

        # Accumulate feedback for next worker
        if gaps and gaps != "__STOP__":
            ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
            entry = f"\n## Iteration {iteration} gaps ({ts})\n{gaps}\n"
            with open(workspace / "feedback.md", "a") as f:
                f.write(entry)
            feedback += entry

    # ── Max iterations reached / stopped ──────────────────────────────────────
    best_iter, best_output = find_best_iteration(workspace)
    write_file(workspace / "final-output.md", best_output)
    save_state(workspace, {"iteration": max_iter, "status": "fail"})

    best_verdict = read_file(workspace / f"iter-{best_iter:02d}" / "verdict.md")
    log(f"⚠️ max iterations reached — best attempt was iter {best_iter}")
    notify(
        args.recipient,
        (
            f"⚠️ checkmate: max iterations ({max_iter}) reached without full PASS\n\n"
            f"Best attempt: iteration {best_iter}\n\n{best_output}\n\n"
            f"---\nFinal judge report:\n{best_verdict}"
        ),
        args.channel,
    )


if __name__ == "__main__":
    main()


path: scripts/workspace.sh
content: #!/usr/bin/env bash
# checkmate/scripts/workspace.sh
# Creates and initializes a checkmate workspace directory.
#
# Usage: workspace.sh <base_dir> [task_description]
#   base_dir: parent directory for the workspace (e.g. /tmp or ~/checkmate-runs)
#   task_description: optional task text to write to task.md
#
# Output: prints the workspace path to stdout

set -euo pipefail

BASE_DIR="${1:-/tmp}"
TASK="${2:-}"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
WORKSPACE="${BASE_DIR}/checkmate-${TIMESTAMP}"

mkdir -p "${WORKSPACE}"

# Write initial state
cat > "${WORKSPACE}/state.json" <<EOF
{"iteration": 0, "status": "running", "createdAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"}
EOF

# Write task if provided
if [ -n "${TASK}" ]; then
  echo "${TASK}" > "${WORKSPACE}/task.md"
fi

# Create feedback file (empty)
touch "${WORKSPACE}/feedback.md"

echo "${WORKSPACE}"


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