ollie tools for wayland task execution

This commit is contained in:
Window Maker 2026-05-16 13:06:18 +02:00
parent a818db8a19
commit 0a295350cd
10 changed files with 1114 additions and 0 deletions

157
tools/wm-port/README.md Normal file
View File

@ -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/<id>` 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/<id>` 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 |

77
tools/wm-port/SUMMARY.md Normal file
View File

@ -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.

0
tools/wm-port/done.txt Normal file
View File

131
tools/wm-port/task-map.sh Normal file
View File

@ -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
}

279
tools/wm-port/wm-dispatch Executable file
View File

@ -0,0 +1,279 @@
#!/usr/bin/env bash
# wm-dispatch: Dispatch a WindowMaker Wayland port task to an agent.
#
# Usage:
# wm-dispatch <task-id> # dispatch one task
# wm-dispatch <task-id> <task-id>... # dispatch multiple in parallel
# wm-dispatch --wave <N> # 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 <task-id> 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 <<PROMPT
You are executing a mechanical Wayland porting task for WindowMaker.
# Task: $tid ($tname)
## Task Description (from ACTION_PLAN.md)
$task_block
## Procedure (Template $tnum)
$template
$ref_tables
## Important Rules
1. Work in the git repository at $ROOT. Create a git branch named "port/$tid" before making changes.
2. Make exactly the changes described. No more, no less.
3. After making changes, verify with the grep commands in the template.
4. Attempt to compile with: cd $ROOT && make -j\$(nproc) 2>&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 <reason>
- FAIL: could not complete because <reason>
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

49
tools/wm-port/wm-done Executable file
View File

@ -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 <task-id> # mark task as done
# wm-done --undo <task-id> # 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] <task-id>"
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

41
tools/wm-port/wm-kill Executable file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env bash
# wm-kill: Kill running wm-port agent sessions.
#
# Usage:
# wm-kill <task-id> # 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 <task-id> | --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

141
tools/wm-port/wm-review Executable file
View File

@ -0,0 +1,141 @@
#!/usr/bin/env bash
# wm-review: Review completed task, approve or reject.
#
# Usage:
# wm-review <task-id> # 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 <task-id> | --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

85
tools/wm-port/wm-status Executable file
View File

@ -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

154
tools/wm-port/wm-tasks Executable file
View File

@ -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 <N> # 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 <task-id> # 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'