diff --git a/tools/wm-port/README.md b/tools/wm-port/README.md new file mode 100644 index 0000000..3c6675b --- /dev/null +++ b/tools/wm-port/README.md @@ -0,0 +1,157 @@ +# wm-port: WindowMaker Wayland Port Dispatcher + +Agent-assisted task execution for the Wayland port, driven by `ACTION_PLAN.md` and `ACTION_PLAN_PROMPTS.md`. + +## Quick Start + +```bash +# Add to PATH (or use full paths) +export PATH="$PWD/tools/wm-port:$PATH" + +# See what's available +wm-tasks + +# Dispatch a single task (foreground — waits for completion) +wm-dispatch B.1 + +# Dispatch and return immediately (background) +wm-dispatch --bg B.1 B.2 B.3 + +# Monitor running agents +wm-status --watch + +# Follow a specific agent's work live +wm-status --follow B.1 + +# Review completed work +wm-review --pending +wm-review B.1 + +# Dispatch all unblocked Wave 1 tasks in parallel +wm-dispatch --wave 1 --bg +``` + +## Scripts + +| Script | Purpose | +|---|---| +| `wm-dispatch` | Dispatch tasks to agent sessions | +| `wm-tasks` | Show task board (status, waves, blockers) | +| `wm-status` | Monitor active agent sessions | +| `wm-review` | Interactive diff review, approve/reject | +| `wm-done` | Manually mark tasks done/undone | +| `wm-kill` | Kill running agent sessions | + +## Workflow + +### 1. Pick tasks + +```bash +wm-tasks --ready # what can be dispatched now? +wm-tasks --wave 1 # what's in wave 1? +wm-tasks B.1 # details on a specific task +``` + +### 2. Dispatch + +```bash +# Single task, wait for result +wm-dispatch A5.1 + +# Preview the prompt without dispatching +wm-dispatch --dry-run A5.1 + +# Multiple tasks in parallel (background) +wm-dispatch --bg A5.1 B.1 B.3 C.1 + +# All ready tasks in a wave +wm-dispatch --wave 1 --bg + +# Next 5 unblocked tasks regardless of wave +wm-dispatch --next 5 --bg + +# Use a different model +wm-dispatch --model anthropic/claude-opus-4 D.1 +``` + +### 3. Monitor + +```bash +# Dashboard (refreshes every 2s) +wm-status --watch + +# Follow one agent's chat live +wm-status --follow A5.1 + +# Kill a stuck agent +wm-kill A5.1 + +# Kill all +wm-kill --all +``` + +### 4. Review + +```bash +# List branches ready for review +wm-review --pending + +# Interactive review (shows diff, offers approve/reject/edit) +wm-review A5.1 +``` + +The review flow: +1. Shows diff stat and colored diff +2. Shows agent's reported status (DONE/STOP/FAIL) +3. Prompts for action: + - **approve** — merges the `port/` branch, marks task done + - **reject** — optionally deletes the branch + - **diff** — full diff in pager + - **log** — agent's full output in pager + - **edit** — opens changed files in `$EDITOR` + - **skip** — come back later + +### 5. Track + +```bash +# Mark a task done manually (e.g., verified by hand) +wm-done G.1 + +# Re-open a task +wm-done --undo G.1 + +# Full board +wm-tasks +``` + +## How It Works + +1. `wm-dispatch` extracts the task block from `ACTION_PLAN.md` and the matching template from `ACTION_PLAN_PROMPTS.md` +2. It composes a prompt: task description + template procedure + reference tables +3. It spawns an ollie agent session (`coding` agent config) via `$OLLIE/s/b` +4. The agent creates a `port/` git branch, makes the changes, verifies, and reports status +5. You review the branch with `wm-review` + +## Files + +``` +tools/wm-port/ + task-map.sh # Task database: ID→template, ID→wave, ID→blockers + done.txt # Completed task IDs (one per line) + logs/ # Agent output logs (one per task) + wm-dispatch # Dispatcher + wm-tasks # Task board + wm-status # Session monitor + wm-review # Diff reviewer + wm-done # Manual done/undo + wm-kill # Session killer +``` + +## Configuration + +| Variable | Default | Description | +|---|---|---| +| `OLLIE` | `~/mnt/ollie` | Ollie mount path | +| `--backend` | `openrouter` | LLM backend | +| `--model` | `anthropic/claude-sonnet-4` | LLM model | +| `--agent` | `coding` | Ollie agent config | diff --git a/tools/wm-port/SUMMARY.md b/tools/wm-port/SUMMARY.md new file mode 100644 index 0000000..dd1200d --- /dev/null +++ b/tools/wm-port/SUMMARY.md @@ -0,0 +1,77 @@ +## `tools/wm-port/` — 6 scripts + 1 data file + +### Architecture + +``` + ┌─────────────┐ + │ ACTION_PLAN │ task descriptions + │ .md │ (file, lines, operation) + └──────┬──────┘ + │ extract_task_block() + ▼ +┌──────────┐ ┌─────────────────┐ ┌───────────────┐ +│ task-map │────▶│ wm-dispatch │◀────│ ACTION_PLAN │ +│ .sh │ │ │ │ _PROMPTS.md │ +│ ID→T,W,B │ │ compose prompt │ │ (templates) │ +└──────────┘ │ spawn agent │ └───────────────┘ + └────────┬────────┘ + │ $OLLIE/s/b + ▼ + ┌─────────────────┐ + │ ollie session │ wm-port-{task-id} + │ (coding agent) │ creates port/{id} branch + └────────┬────────┘ + │ + ┌───────────────┼───────────────┐ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────┐ + │wm-status │ │wm-review │ │ wm-kill │ + │ monitor │ │ diff/ │ │ abort │ + │ --watch │ │ approve │ │ session │ + └──────────┘ └──────────┘ └──────────┘ +``` + +### Scripts + +| Script | What you type | What happens | +|---|---|---| +| **`wm-tasks`** | `wm-tasks` | Task board: 52 tasks grouped by wave, colored by status (✓ done, ○ ready, ● running, • blocked) | +| **`wm-dispatch`** | `wm-dispatch B.1` | Extracts task B.1 from ACTION_PLAN.md, matches to Template 3, composes full prompt with reference tables, spawns a `coding` agent session | +| **`wm-status`** | `wm-status --watch` | Live dashboard of all running `wm-port-*` sessions, refreshing every 2s | +| **`wm-review`** | `wm-review B.1` | Interactive review: shows diff, agent status, then prompts for approve/reject/edit/skip | +| **`wm-done`** | `wm-done G.1` | Manually mark verification tasks done (or `--undo` to reopen) | +| **`wm-kill`** | `wm-kill --all` | Kill stuck agent sessions | + +### Your typical session + +```bash +export PATH="$PWD/tools/wm-port:$PATH" + +# 1. See what's ready +wm-tasks --ready + +# 2. Dispatch (pick one) +wm-dispatch B.1 # single, foreground (wait for result) +wm-dispatch --bg A5.1 B.1 B.3 C.1 # batch, background +wm-dispatch --wave 1 --bg # entire wave 1 in parallel +wm-dispatch --next 5 --bg # next 5 unblocked tasks + +# 3. Monitor (in another terminal) +wm-status --watch # dashboard +wm-status --follow B.1 # tail one agent's chat + +# 4. Review +wm-review --pending # list branches ready +wm-review B.1 # interactive: [a]pprove [r]eject [d]iff [l]og [e]dit [s]kip + +# 5. Repeat +wm-tasks # updated board, blockers auto-resolved +``` + +### Key design decisions + +- **No template variable filling** — the prompt includes both the raw task block *and* the template procedure. The agent connects them. This is more robust than regex substitution on `{{FILE}}` placeholders, and the agent handles it trivially. +- **Git branch per task** — the agent creates `port/{id}` branches, keeping main clean. `wm-review` merges on approve. +- **`done.txt` is the single source of truth** — one task ID per line. Blocker resolution is computed from this file. `wm-review --approve` appends automatically. +- **`--dry-run`** prints the exact prompt that would be sent, so you can inspect/tweak before committing API tokens. +- **Foreground vs background** — foreground (`wm-dispatch B.1`) blocks until done and auto-parses DONE/STOP/FAIL from the agent's output. Background (`--bg`) returns session paths immediately for monitoring. diff --git a/tools/wm-port/done.txt b/tools/wm-port/done.txt new file mode 100644 index 0000000..e69de29 diff --git a/tools/wm-port/task-map.sh b/tools/wm-port/task-map.sh new file mode 100644 index 0000000..1bdf30d --- /dev/null +++ b/tools/wm-port/task-map.sh @@ -0,0 +1,131 @@ +#!/bin/bash +# Task ID → Template number mapping. +# Sourced by other scripts. Provides task_template() and task_wave(). + +# Template mapping: task ID → template number +# Template 1: XEvent* → WMEvent* signature change +# Template 2: _x11_raw elimination +# Template 3: Xlib → wm_backend vtable +# Template 4: Guard X11 block in WINGs (if scr->display) +# Template 5: Split into _x11/_wl pair +# Template 6: Xlib drawing → WINGs render backend +# Template 7: Remove #ifdef guard +# Template 8: WPrefs.app Xlib → WINGs API +# Template 9: Verification only + +declare -A TASK_TEMPLATE=( + # Phase F: wrlib + [F.2]=9 [F.3]=9 [F.4]=9 [F.5]=4 + + # Phase A: Core Window Management + [A4.1]=7 + [A5.1]=1 [A5.2]=2 + + # Phase B: Dock & App Icons + [B.1]=3 [B.2]=7 [B.3]=1 + + # Phase C: Visual Polish + [C.1]=3 [C.3]=9 + + # Phase D: WINGs Widget Porting + [D.1]=4 [D.2]=4 [D.3]=5 [D.4]=6 [D.5]=5 [D.6]=5 + [D.7]=6 [D.8]=6 [D.9]=6 [D.10]=6 [D.11]=4 [D.12]=6 + + # Phase E: Dialogs & Panels + [E.1]=2 [E.2]=2 [E.3]=1 [E.4]=1 + [E.5a]=8 [E.5b]=8 [E.5c]=8 [E.5d]=8 [E.5e]=8 + [E.5f]=8 [E.5g]=8 [E.5h]=8 [E.5i]=8 [E.5j]=8 [E.5k]=8 + + # Phase G: Session & Persistence + [G.1]=9 [G.2]=3 + + # Phase I: EWMH / Foreign Toplevel + [I.1]=9 [I.2]=9 + + # Phase H: Advanced Compositor + [H.1]=7 [H.5]=9 + + # Remaining src/ cleanup + [R.1]=2 [R.2]=2 [R.3]=2 [R.4]=3 [R.5]=3 + [R.6]=7 [R.7]=7 +) + +# Wave assignments +declare -A TASK_WAVE=( + # Immediate + [F.2]=0 [F.3]=0 [F.4]=0 [F.5]=0 + + # Wave 1 + [A4.1]=1 [A5.1]=1 [B.1]=1 [B.2]=1 [B.3]=1 + [C.1]=1 [C.3]=1 + [D.1]=1 [D.3]=1 [D.5]=1 [D.6]=1 [D.10]=1 [D.11]=1 [D.12]=1 + [E.3]=1 [E.4]=1 + [G.1]=1 [G.2]=1 + [I.1]=1 [I.2]=1 + [H.1]=1 [H.5]=1 + [R.1]=1 [R.2]=1 [R.3]=1 [R.4]=1 [R.5]=1 [R.6]=1 [R.7]=1 + + # Wave 2 + [A5.2]=2 [D.2]=2 [D.4]=2 [D.7]=2 [D.8]=2 [D.9]=2 + [E.1]=2 [E.2]=2 + + # Wave 3 + [E.5a]=3 [E.5b]=3 [E.5c]=3 [E.5d]=3 [E.5e]=3 + [E.5f]=3 [E.5g]=3 [E.5h]=3 [E.5i]=3 [E.5j]=3 [E.5k]=3 +) + +# Blocker mapping: task ID → space-separated list of prerequisite task IDs +declare -A TASK_BLOCKERS=( + [F.3]="F.2" + [F.5]="F.2" + [A5.2]="A5.1" + [D.2]="D.1" [D.4]="D.1" [D.7]="D.1" + [D.8]="D.5" [D.9]="D.5" + [E.2]="B.3 E.3" + [E.5a]="D.1 D.2 D.3 D.4 D.5 D.6 D.7 D.8 D.9 D.10" + [E.5b]="D.1 D.2 D.3 D.4 D.5 D.6 D.7 D.8 D.9 D.10" + [E.5c]="D.1 D.2 D.3 D.4 D.5 D.6 D.7 D.8 D.9 D.10" + [E.5d]="D.1 D.2 D.3 D.4 D.5 D.6 D.7 D.8 D.9 D.10" + [E.5e]="D.1 D.2 D.3 D.4 D.5 D.6 D.7 D.8 D.9 D.10" + [E.5f]="D.1 D.2 D.3 D.4 D.5 D.6 D.7 D.8 D.9 D.10" + [E.5g]="D.1 D.2 D.3 D.4 D.5 D.6 D.7 D.8 D.9 D.10" + [E.5h]="D.1 D.2 D.3 D.4 D.5 D.6 D.7 D.8 D.9 D.10" + [E.5i]="D.1 D.2 D.3 D.4 D.5 D.6 D.7 D.8 D.9 D.10" + [E.5j]="D.1 D.2 D.3 D.4 D.5 D.6 D.7 D.8 D.9 D.10" + [E.5k]="D.1 D.2 D.3 D.4 D.5 D.6 D.7 D.8 D.9 D.10" +) + +# Template names for display +declare -A TEMPLATE_NAME=( + [1]="Signature XEvent*→WMEvent*" + [2]="Eliminate _x11_raw" + [3]="Xlib→wm_backend vtable" + [4]="Guard X11 block (if scr->display)" + [5]="Split into _x11/_wl pair" + [6]="Xlib drawing→render backend" + [7]="Remove #ifdef guard" + [8]="WPrefs Xlib→WINGs API" + [9]="Verification only" +) + +# Get all task IDs in a stable order +task_all_ids() { + printf '%s\n' "${!TASK_TEMPLATE[@]}" | sort -t. -k1,1 -k2,2n +} + +# Check if a task is done +task_is_done() { + local id="$1" done_file="$2" + [ -f "$done_file" ] && grep -qxF "$id" "$done_file" +} + +# Check if a task is unblocked (all blockers done) +task_is_unblocked() { + local id="$1" done_file="$2" + local blockers="${TASK_BLOCKERS[$id]:-}" + [ -z "$blockers" ] && return 0 + for dep in $blockers; do + task_is_done "$dep" "$done_file" || return 1 + done + return 0 +} diff --git a/tools/wm-port/wm-dispatch b/tools/wm-port/wm-dispatch new file mode 100755 index 0000000..2634b46 --- /dev/null +++ b/tools/wm-port/wm-dispatch @@ -0,0 +1,279 @@ +#!/usr/bin/env bash +# wm-dispatch: Dispatch a WindowMaker Wayland port task to an agent. +# +# Usage: +# wm-dispatch # dispatch one task +# wm-dispatch ... # dispatch multiple in parallel +# wm-dispatch --wave # dispatch all unblocked tasks in wave N +# wm-dispatch --next [N] # dispatch next N unblocked tasks (default 1) +# +# Options: +# --dry-run Print the prompt instead of dispatching +# --backend B Override backend (default: openrouter) +# --model M Override model (default: anthropic/claude-sonnet-4) +# --keep Don't auto-kill session on completion +# --bg Submit and return immediately (print session paths) + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +TOOLS="$ROOT/tools/wm-port" +MOUNT="${OLLIE:-$HOME/mnt/ollie}" +DONE_FILE="$TOOLS/done.txt" +LOG_DIR="$TOOLS/logs" + +source "$TOOLS/task-map.sh" + +# Defaults +backend="openrouter" +model="anthropic/claude-sonnet-4" +dry_run=0 +keep=0 +bg=0 +wave="" +next=0 +task_ids=() + +die() { printf 'wm-dispatch: %s\n' "$*" >&2; exit 1; } + +while [ $# -gt 0 ]; do + case "$1" in + --dry-run) dry_run=1 ;; + --backend) backend="$2"; shift ;; + --model) model="$2"; shift ;; + --keep) keep=1 ;; + --bg) bg=1 ;; + --wave) wave="$2"; shift ;; + --next) next="${2:-1}"; shift ;; + --help|-h) sed -n '2,/^$/{ s/^# //; s/^#//; p; }' "$0"; exit 0 ;; + -*) die "unknown flag: $1" ;; + *) task_ids+=("$1") ;; + esac + shift +done + +mkdir -p "$LOG_DIR" +touch "$DONE_FILE" + +# Resolve task list from --wave or --next +if [ -n "$wave" ]; then + while IFS= read -r tid; do + [ "${TASK_WAVE[$tid]:-}" = "$wave" ] || continue + task_is_done "$tid" "$DONE_FILE" && continue + task_is_unblocked "$tid" "$DONE_FILE" || continue + task_ids+=("$tid") + done < <(task_all_ids) + [ ${#task_ids[@]} -eq 0 ] && die "no unblocked tasks in wave $wave" +elif [ "$next" -gt 0 ]; then + count=0 + while IFS= read -r tid; do + task_is_done "$tid" "$DONE_FILE" && continue + task_is_unblocked "$tid" "$DONE_FILE" || continue + task_ids+=("$tid") + (( ++count >= next )) && break + done < <(task_all_ids) + [ ${#task_ids[@]} -eq 0 ] && die "no unblocked tasks remaining" +fi + +[ ${#task_ids[@]} -eq 0 ] && die "no task IDs specified. Use: wm-dispatch or --wave N or --next N" + +# Validate all task IDs +for tid in "${task_ids[@]}"; do + [ -n "${TASK_TEMPLATE[$tid]:-}" ] || die "unknown task ID: $tid" + task_is_done "$tid" "$DONE_FILE" && die "task $tid is already done" + task_is_unblocked "$tid" "$DONE_FILE" || { + blockers="${TASK_BLOCKERS[$tid]}" + pending="" + for dep in $blockers; do + task_is_done "$dep" "$DONE_FILE" || pending="$pending $dep" + done + die "task $tid is blocked by:$pending" + } +done + +# ── Extract task block from ACTION_PLAN.md ── +# Finds the section for a given task ID (everything from its header to the next +# header of equal or higher level, or next --- separator). +extract_task_block() { + local tid="$1" + local plan="$ROOT/ACTION_PLAN.md" + + awk -v tid="$tid" ' + BEGIN { found=0; level=0 } + /^#{1,6} / { + if (found) { + match($0, /^#+/) + if (RLENGTH <= level) exit + } + if (index($0, tid) > 0) { + match($0, /^#+/) + level = RLENGTH + found = 1 + } + } + /^---/ { if (found) exit } + found { print } + ' "$plan" +} + +# ── Extract template block from ACTION_PLAN_PROMPTS.md ── +extract_template() { + local tnum="$1" + local prompts="$ROOT/ACTION_PLAN_PROMPTS.md" + + awk -v tnum="$tnum" ' + BEGIN { found=0; in_code=0 } + /^## Template / { + if (found && !in_code) exit + if ($0 ~ "Template " tnum ":" || $0 ~ "Template " tnum " ") { + found=1 + next + } + } + /^## Template [0-9]/ { if (found) exit } + /^---$/ { if (found && !in_code) exit } + found { print } + ' "$prompts" +} + +# ── Extract reference tables ── +extract_reference_tables() { + local prompts="$ROOT/ACTION_PLAN_PROMPTS.md" + awk ' + /^## Reference Tables/,/^## Template 1/ { + if (/^## Template 1/) exit + print + } + ' "$prompts" +} + +# ── Build the full prompt for a task ── +build_prompt() { + local tid="$1" + local tnum="${TASK_TEMPLATE[$tid]}" + local tname="${TEMPLATE_NAME[$tnum]}" + + local task_block + task_block=$(extract_task_block "$tid") + [ -n "$task_block" ] || die "could not extract task block for $tid from ACTION_PLAN.md" + + local template + template=$(extract_template "$tnum") + + local ref_tables + ref_tables=$(extract_reference_tables) + + cat <&1 | tail -30 +5. If compilation fails, fix the errors. If you cannot fix them, revert and report. +6. If you hit a STOP condition (missing vtable slot, missing API), revert changes and report exactly what is missing. +7. When done, run: git add -A && git diff --cached --stat +8. Report the final status as one of: + - DONE: task completed, compiles clean + - STOP: blocked on + - FAIL: could not complete because + +Line numbers in ACTION_PLAN.md may have drifted. Use grep to find current line numbers before editing. +PROMPT +} + +# ── Dispatch a single task ── +dispatch_one() { + local tid="$1" + local prompt + prompt=$(build_prompt "$tid") + + if [ "$dry_run" -eq 1 ]; then + echo "════════════════════════════════════════" + echo "TASK: $tid (Template ${TASK_TEMPLATE[$tid]}: ${TEMPLATE_NAME[${TASK_TEMPLATE[$tid]}]})" + echo "════════════════════════════════════════" + echo "$prompt" + echo + return 0 + fi + + local session_name="wm-port-${tid}" + local flags=( + -name "$session_name" + -cwd "$ROOT" + -agent coding + -backend "$backend" + -model "$model" + ) + [ "$keep" -eq 1 ] && flags+=(-keep) + [ "$bg" -eq 1 ] && flags+=(-bg) + + printf '\033[1;36m▶ Dispatching %s\033[0m (%s)\n' \ + "$tid" "${TEMPLATE_NAME[${TASK_TEMPLATE[$tid]}]}" >&2 + + if [ "$bg" -eq 1 ]; then + local session_path + session_path=$(printf '%s' "$prompt" | "$MOUNT/s/b" "${flags[@]}") + printf '%s\t%s\n' "$tid" "$session_path" + printf '\033[1;33m → %s\033[0m\n' "$session_path" >&2 + else + local result + result=$(printf '%s' "$prompt" | "$MOUNT/s/b" "${flags[@]}" 2>&1) || true + + # Save log + printf '%s\n' "$result" > "$LOG_DIR/${tid}.log" + + # Parse result status + local status="UNKNOWN" + if echo "$result" | grep -q '^DONE\b\|^\*\*DONE\*\*\|Status:.*DONE'; then + status="DONE" + echo "$tid" >> "$DONE_FILE" + printf '\033[1;32m✓ %s: DONE\033[0m\n' "$tid" >&2 + elif echo "$result" | grep -q '^STOP\b\|^\*\*STOP\*\*\|Status:.*STOP'; then + status="STOP" + printf '\033[1;33m⚠ %s: STOP\033[0m\n' "$tid" >&2 + elif echo "$result" | grep -q '^FAIL\b\|^\*\*FAIL\*\*\|Status:.*FAIL'; then + status="FAIL" + printf '\033[1;31m✗ %s: FAIL\033[0m\n' "$tid" >&2 + else + printf '\033[1;35m? %s: status unclear — check log\033[0m\n' "$tid" >&2 + fi + + printf ' Log: %s\n' "$LOG_DIR/${tid}.log" >&2 + fi +} + +# ── Main ── +if [ ${#task_ids[@]} -eq 1 ]; then + dispatch_one "${task_ids[0]}" +else + # Multiple tasks: dispatch in parallel (background mode) + printf '\033[1;36m▶ Dispatching %d tasks in parallel\033[0m\n' ${#task_ids[@]} >&2 + + pids=() + for tid in "${task_ids[@]}"; do + dispatch_one "$tid" & + pids+=($!) + done + + # Wait for all + rc=0 + for pid in "${pids[@]}"; do + wait "$pid" || rc=1 + done + exit $rc +fi diff --git a/tools/wm-port/wm-done b/tools/wm-port/wm-done new file mode 100755 index 0000000..7ab3fd8 --- /dev/null +++ b/tools/wm-port/wm-done @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# wm-done: Manually mark a task as done (for tasks completed outside the dispatcher). +# +# Usage: +# wm-done # mark task as done +# wm-done --undo # unmark task (re-open it) + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +TOOLS="$ROOT/tools/wm-port" +DONE_FILE="$TOOLS/done.txt" + +source "$TOOLS/task-map.sh" +touch "$DONE_FILE" + +die() { printf 'wm-done: %s\n' "$*" >&2; exit 1; } + +undo=0 +while [ $# -gt 0 ]; do + case "$1" in + --undo) undo=1 ;; + --help|-h) sed -n '2,/^$/{ s/^# //; s/^#//; p; }' "$0"; exit 0 ;; + -*) die "unknown flag: $1" ;; + *) break ;; + esac + shift +done + +[ $# -lt 1 ] && die "usage: wm-done [--undo] " +tid="$1" +[ -n "${TASK_TEMPLATE[$tid]:-}" ] || die "unknown task ID: $tid" + +if [ "$undo" -eq 1 ]; then + if task_is_done "$tid" "$DONE_FILE"; then + grep -vxF "$tid" "$DONE_FILE" > "$DONE_FILE.tmp" + mv "$DONE_FILE.tmp" "$DONE_FILE" + printf '\033[1;33m↺ Task %s re-opened.\033[0m\n' "$tid" + else + echo "Task $tid is not marked as done." + fi +else + if task_is_done "$tid" "$DONE_FILE"; then + echo "Task $tid is already done." + else + echo "$tid" >> "$DONE_FILE" + printf '\033[1;32m✓ Task %s marked as done.\033[0m\n' "$tid" + fi +fi diff --git a/tools/wm-port/wm-kill b/tools/wm-port/wm-kill new file mode 100755 index 0000000..cb9a1ff --- /dev/null +++ b/tools/wm-port/wm-kill @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# wm-kill: Kill running wm-port agent sessions. +# +# Usage: +# wm-kill # kill session for a specific task +# wm-kill --all # kill all wm-port sessions + +set -euo pipefail + +MOUNT="${OLLIE:-$HOME/mnt/ollie}" +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +TOOLS="$ROOT/tools/wm-port" + +source "$TOOLS/task-map.sh" + +die() { printf 'wm-kill: %s\n' "$*" >&2; exit 1; } + +if [ "${1:-}" = "--all" ]; then + killed=0 + while IFS=$'\t' read -r sid state cwd backend model; do + [[ "$sid" == wm-port-* ]] || continue + echo "kill" > "$MOUNT/s/$sid/ctl" 2>/dev/null && { + printf 'Killed %s\n' "$sid" + (( ++killed )) + } + done < <(cat "$MOUNT/s/idx" 2>/dev/null) + [ "$killed" -eq 0 ] && echo "No active wm-port sessions." + exit 0 +fi + +[ $# -lt 1 ] && die "usage: wm-kill | --all" + +tid="$1" +sid="wm-port-${tid}" + +if [ -d "$MOUNT/s/$sid" ]; then + echo "kill" > "$MOUNT/s/$sid/ctl" 2>/dev/null + printf '\033[1;31mKilled session %s\033[0m\n' "$sid" +else + echo "No active session for task $tid" +fi diff --git a/tools/wm-port/wm-review b/tools/wm-port/wm-review new file mode 100755 index 0000000..dcc2153 --- /dev/null +++ b/tools/wm-port/wm-review @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +# wm-review: Review completed task, approve or reject. +# +# Usage: +# wm-review # review a specific task +# wm-review --pending # list tasks with branches ready for review +# +# Actions (interactive): +# a/approve - commit on branch, mark task done +# r/reject - reset branch, discard changes +# d/diff - show full diff again +# l/log - show agent log +# s/skip - leave for later +# e/edit - open changed files in $EDITOR + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +TOOLS="$ROOT/tools/wm-port" +DONE_FILE="$TOOLS/done.txt" +LOG_DIR="$TOOLS/logs" + +source "$TOOLS/task-map.sh" +touch "$DONE_FILE" + +die() { printf 'wm-review: %s\n' "$*" >&2; exit 1; } + +# List pending reviews +if [ "${1:-}" = "--pending" ]; then + cd "$ROOT" + found=0 + for branch in $(git branch --list 'port/*' --format='%(refname:short)' 2>/dev/null); do + tid="${branch#port/}" + if ! task_is_done "$tid" "$DONE_FILE" 2>/dev/null; then + # Count changes + changes=$(git diff main..."$branch" --stat 2>/dev/null | tail -1) + printf ' %-10s %s\n' "$tid" "$changes" + found=1 + fi + done + [ "$found" -eq 0 ] && echo "No branches pending review." + exit 0 +fi + +[ $# -lt 1 ] && die "usage: wm-review | --pending" + +tid="$1" +[ -n "${TASK_TEMPLATE[$tid]:-}" ] || die "unknown task ID: $tid" + +cd "$ROOT" +branch="port/$tid" + +# Check branch exists +git rev-parse --verify "$branch" &>/dev/null || die "branch '$branch' does not exist" + +# Show summary +printf '\033[1m=== Review: Task %s ===\033[0m\n' "$tid" +printf 'Template: %s (%s)\n' "${TASK_TEMPLATE[$tid]}" "${TEMPLATE_NAME[${TASK_TEMPLATE[$tid]}]}" +printf 'Branch: %s\n\n' "$branch" + +# Determine base branch +base="HEAD" +if git rev-parse --verify main &>/dev/null; then + base="main" +elif git rev-parse --verify master &>/dev/null; then + base="master" +fi + +# Show diff stat +printf '\033[1;4mChanges:\033[0m\n' +git diff "$base"..."$branch" --stat +echo + +# Show the actual diff +printf '\033[1;4mDiff:\033[0m\n' +git diff "$base"..."$branch" --color=always | head -200 +echo + +if [ -f "$LOG_DIR/${tid}.log" ]; then + # Show agent's final status from log + printf '\033[1;4mAgent status:\033[0m\n' + grep -E '^(DONE|STOP|FAIL|\*\*DONE|\*\*STOP|\*\*FAIL|Status:)' "$LOG_DIR/${tid}.log" | tail -5 + echo +fi + +# Interactive review loop +while true; do + printf '\033[1;36m[a]pprove [r]eject [d]iff [l]og [e]dit [s]kip > \033[0m' + read -r action < /dev/tty + + case "$action" in + a|approve) + # Merge branch into current + current=$(git branch --show-current) + git merge --no-ff -m "port($tid): ${TEMPLATE_NAME[${TASK_TEMPLATE[$tid]}]}" "$branch" || { + echo "Merge conflict. Resolve manually, then run: echo '$tid' >> $DONE_FILE" + exit 1 + } + echo "$tid" >> "$DONE_FILE" + printf '\033[1;32m✓ Task %s approved and merged.\033[0m\n' "$tid" + + # Optionally delete the branch + printf 'Delete branch %s? [y/N] ' "$branch" + read -r del < /dev/tty + [ "$del" = "y" ] || [ "$del" = "Y" ] && git branch -d "$branch" 2>/dev/null + break + ;; + r|reject) + printf 'Delete branch %s? [y/N] ' "$branch" + read -r del < /dev/tty + if [ "$del" = "y" ] || [ "$del" = "Y" ]; then + git branch -D "$branch" 2>/dev/null + printf '\033[1;31m✗ Task %s rejected, branch deleted.\033[0m\n' "$tid" + else + printf '\033[1;31m✗ Task %s rejected, branch kept.\033[0m\n' "$tid" + fi + break + ;; + d|diff) + git diff "$base"..."$branch" --color=always | ${PAGER:-less -R} + ;; + l|log) + if [ -f "$LOG_DIR/${tid}.log" ]; then + ${PAGER:-less -R} "$LOG_DIR/${tid}.log" + else + echo "No log file for $tid" + fi + ;; + e|edit) + files=$(git diff "$base"..."$branch" --name-only) + ${EDITOR:-vi} $files + ;; + s|skip) + printf 'Skipping %s.\n' "$tid" + break + ;; + *) + echo "Unknown action: $action" + ;; + esac +done diff --git a/tools/wm-port/wm-status b/tools/wm-port/wm-status new file mode 100755 index 0000000..df311d5 --- /dev/null +++ b/tools/wm-port/wm-status @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# wm-status: Live status of running wm-port agent sessions. +# +# Usage: +# wm-status # show all active wm-port sessions +# wm-status --watch # refresh every 2 seconds +# wm-status --follow ID # tail the chat log for a task + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +TOOLS="$ROOT/tools/wm-port" +MOUNT="${OLLIE:-$HOME/mnt/ollie}" + +source "$TOOLS/task-map.sh" + +watch=0 +follow="" + +while [ $# -gt 0 ]; do + case "$1" in + --watch|-w) watch=1 ;; + --follow|-f) follow="$2"; shift ;; + --help|-h) sed -n '2,/^$/{ s/^# //; s/^#//; p; }' "$0"; exit 0 ;; + *) follow="$1" ;; + esac + shift +done + +# Follow mode: tail a specific task's session +if [ -n "$follow" ]; then + session_dir="$MOUNT/s/wm-port-${follow}" + [ -d "$session_dir" ] || { + echo "No active session for task $follow" >&2 + exit 1 + } + printf '\033[1mFollowing task %s (Ctrl-C to stop)\033[0m\n\n' "$follow" + exec tail -f "$session_dir/chat" +fi + +show_status() { + local found=0 + + # Read session index + while IFS=$'\t' read -r sid state cwd backend model; do + # Filter to wm-port sessions + [[ "$sid" == wm-port-* ]] || continue + + local tid="${sid#wm-port-}" + local tnum="${TASK_TEMPLATE[$tid]:-?}" + local tname="${TEMPLATE_NAME[$tnum]:-unknown}" + + local color="\033[33m" # yellow for thinking/calling + [ "$state" = "idle" ] && color="\033[32m" # green for idle + [[ "$state" == failed* ]] && color="\033[31m" # red for failed + + # Get usage if available + local usage="" + if [ -f "$MOUNT/s/$sid/usage" ]; then + usage=$(cat "$MOUNT/s/$sid/usage" 2>/dev/null | tr '\n' ' ') + fi + + printf ' %b%-8s\033[0m %b%-20s\033[0m T%s %s' \ + "$color" "$tid" "$color" "$state" "$tnum" "$tname" + [ -n "$usage" ] && printf ' \033[90m(%s)\033[0m' "$usage" + printf '\n' + + found=1 + done < <(cat "$MOUNT/s/idx" 2>/dev/null) + + [ "$found" -eq 0 ] && printf ' \033[90m(no active wm-port sessions)\033[0m\n' +} + +if [ "$watch" -eq 1 ]; then + while true; do + clear + printf '\033[1mwm-port Active Sessions\033[0m (%s)\n\n' "$(date +%H:%M:%S)" + show_status + printf '\n\033[90mRefreshing every 2s. Ctrl-C to stop.\033[0m\n' + sleep 2 + done +else + printf '\033[1mwm-port Active Sessions\033[0m\n\n' + show_status +fi diff --git a/tools/wm-port/wm-tasks b/tools/wm-port/wm-tasks new file mode 100755 index 0000000..95f7e46 --- /dev/null +++ b/tools/wm-port/wm-tasks @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +# wm-tasks: Show task status for the WindowMaker Wayland port. +# +# Usage: +# wm-tasks # show all tasks grouped by wave +# wm-tasks --wave # show tasks in wave N only +# wm-tasks --ready # show only unblocked, undone tasks +# wm-tasks --done # show only completed tasks +# wm-tasks --blocked # show only blocked tasks +# wm-tasks # show details for one task + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +TOOLS="$ROOT/tools/wm-port" +DONE_FILE="$TOOLS/done.txt" +LOG_DIR="$TOOLS/logs" + +source "$TOOLS/task-map.sh" +touch "$DONE_FILE" + +filter="all" +filter_wave="" +detail_id="" + +while [ $# -gt 0 ]; do + case "$1" in + --wave) filter_wave="$2"; shift ;; + --ready) filter="ready" ;; + --done) filter="done" ;; + --blocked) filter="blocked" ;; + --help|-h) sed -n '2,/^$/{ s/^# //; s/^#//; p; }' "$0"; exit 0 ;; + -*) echo "wm-tasks: unknown flag: $1" >&2; exit 1 ;; + *) detail_id="$1" ;; + esac + shift +done + +# Detail view for a single task +if [ -n "$detail_id" ]; then + [ -n "${TASK_TEMPLATE[$detail_id]:-}" ] || { echo "Unknown task: $detail_id" >&2; exit 1; } + tnum="${TASK_TEMPLATE[$detail_id]}" + wave="${TASK_WAVE[$detail_id]:-?}" + blockers="${TASK_BLOCKERS[$detail_id]:-none}" + + if task_is_done "$detail_id" "$DONE_FILE"; then + status="\033[1;32mDONE\033[0m" + elif task_is_unblocked "$detail_id" "$DONE_FILE"; then + status="\033[1;36mREADY\033[0m" + else + status="\033[1;33mBLOCKED\033[0m" + fi + + printf 'Task: %s\n' "$detail_id" + printf 'Template: %s (%s)\n' "$tnum" "${TEMPLATE_NAME[$tnum]}" + printf 'Wave: %s\n' "$wave" + printf 'Blockers: %s\n' "$blockers" + printf 'Status: %b\n' "$status" + + if [ -f "$LOG_DIR/${detail_id}.log" ]; then + printf 'Log: %s\n' "$LOG_DIR/${detail_id}.log" + echo + echo "--- Last 20 lines of log ---" + tail -20 "$LOG_DIR/${detail_id}.log" + fi + + # Check for active session + local_session="$OLLIE/s/wm-port-${detail_id}" + if [ -d "${OLLIE:-$HOME/mnt/ollie}/s/wm-port-${detail_id}" ]; then + state=$(cat "${OLLIE:-$HOME/mnt/ollie}/s/wm-port-${detail_id}/state" 2>/dev/null || echo "unknown") + printf 'Session: wm-port-%s (%s)\n' "$detail_id" "$state" + fi + exit 0 +fi + +# Summary view +total=0 +done_count=0 +ready_count=0 +blocked_count=0 + +# Collect stats +while IFS= read -r tid; do + (( ++total )) + if task_is_done "$tid" "$DONE_FILE"; then + (( ++done_count )) + elif task_is_unblocked "$tid" "$DONE_FILE"; then + (( ++ready_count )) + else + (( ++blocked_count )) + fi +done < <(task_all_ids) + +printf '\033[1mWindowMaker Wayland Port: %d/%d tasks done, %d ready, %d blocked\033[0m\n\n' \ + "$done_count" "$total" "$ready_count" "$blocked_count" + +# Print by wave +for w in 0 1 2 3; do + [ -n "$filter_wave" ] && [ "$filter_wave" != "$w" ] && continue + + wave_label="Wave $w" + [ "$w" -eq 0 ] && wave_label="Immediate" + + wave_tasks=() + while IFS= read -r tid; do + [ "${TASK_WAVE[$tid]:-}" = "$w" ] || continue + wave_tasks+=("$tid") + done < <(task_all_ids) + + [ ${#wave_tasks[@]} -eq 0 ] && continue + + printed_header=0 + for tid in "${wave_tasks[@]}"; do + tnum="${TASK_TEMPLATE[$tid]}" + + if task_is_done "$tid" "$DONE_FILE"; then + [ "$filter" = "ready" ] || [ "$filter" = "blocked" ] && continue + icon="\033[32m✓\033[0m" + label="" + elif task_is_unblocked "$tid" "$DONE_FILE"; then + [ "$filter" = "done" ] || [ "$filter" = "blocked" ] && continue + icon="\033[36m○\033[0m" + label="" + # Check if running + if [ -d "${OLLIE:-$HOME/mnt/ollie}/s/wm-port-${tid}" ]; then + state=$(cat "${OLLIE:-$HOME/mnt/ollie}/s/wm-port-${tid}/state" 2>/dev/null || echo "?") + icon="\033[33m●\033[0m" + label=" [$state]" + fi + else + [ "$filter" = "done" ] || [ "$filter" = "ready" ] && continue + icon="\033[90m•\033[0m" + blockers="${TASK_BLOCKERS[$tid]:-}" + pending="" + for dep in $blockers; do + task_is_done "$dep" "$DONE_FILE" || pending="$pending $dep" + done + label="\033[90m blocked by:$pending\033[0m" + fi + + if [ "$printed_header" -eq 0 ]; then + printf '\033[1;4m%s\033[0m\n' "$wave_label" + printed_header=1 + fi + + printf ' %b %-8s T%s %-35s%b\n' \ + "$icon" "$tid" "$tnum" "${TEMPLATE_NAME[$tnum]}" "$label" + done + + [ "$printed_header" -eq 1 ] && echo +done + +# Legend +printf '\033[90mLegend: \033[32m✓\033[90m done \033[36m○\033[90m ready \033[33m●\033[90m running \033[90m• blocked\033[0m\n'