8000 fix: make orchestration work when wmux isn't on PATH (fall back to $WMUX_CLI) by tawman · Pull Request #5 · amirlehmam/wmux-orchestrator · GitHub
[go: up one dir, main page]

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

8000
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Keep shell scripts LF so their shebangs run under bash even on Windows with
# core.autocrlf=true (a CRLF shebang breaks `#!/usr/bin/env bash`).
*.sh text eo 10BC8 l=lf
4 changes: 4 additions & 0 deletions scripts/detect-wmux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
# Exit 0 + print "available" if wmux responds to ping.
# Exit 1 + print "unavailable" if not.

# Make bare `wmux` resolvable when it isn't on PATH (falls back to $WMUX_CLI),
# so detection succeeds via the injected CLI even on an un-patched wmux.
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/wmux-resolve.sh"

if command -v wmux &>/dev/null; then
result=$(wmux ping 2>/dev/null)
if [ "$result" = "pong" ]; then
Expand Down
3 changes: 3 additions & 0 deletions scripts/orchestration-state.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ ORCH_BASE="${TMPDIR:-/tmp}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
JSON_TOOL="$SCRIPT_DIR/json-tool.js"

# Make bare `wmux` resolvable when it isn't on PATH (falls back to $WMUX_CLI).
source "$SCRIPT_DIR/wmux-resolve.sh"

# Find the active orchestration directory (most recent state.json with status "running")
find_active_orch() {
local latest=""
Expand Down
14 changes: 14 additions & 0 deletions scripts/wmux-resolve.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# wmux-resolve.sh — make `wmux` callable even where it isn't on PATH.
#
# The orchestrator calls bare `wmux` from non-interactive shells (Claude Code's
# Bash tool, these hook scripts). A patched wmux puts a `wmux` shim on those
# shells' PATH automatically, but on an un-patched/upstream wmux it isn't there.
# In that case fall back to the $WMUX_CLI script wmux injects into every shell it
# spawns, so `wmux ...` still works. No-op when a real `wmux` is already on PATH.
#
# Defining a function makes `command -v wmux` succeed too, so the callers'
# existing `command -v wmux` guards pass without change.
if ! command -v wmux >/dev/null 2>&1 && [ -n "${WMUX_CLI:-}" ]; then
wmux() { node "$WMUX_CLI" "$@"; }
fi
124 changes: 101 additions & 23 deletions skills/orchestrate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ Template (omit sections that don't apply — don't include placeholder text, onl
> names verbatim. Do not invent alternatives.

## Agents bound by this contract
- agent-a ([role])
- agent-b ([role])
- agent a ([role])
- agent b ([role])

## Shared HTML class names
_(fill this section when at least one agent writes HTML and another writes CSS)_
Expand All @@ -139,8 +139,8 @@ _(fill this section when at least one agent writes HTML and another writes CSS)_

## Shared DOM IDs and mount points
_(fill when one agent writes HTML scaffolding that another agent fills via JS)_
- `#wave-sim-root` — empty div in hero; agent-c mounts the simulator into it
- `aside.activity-rail` — empty aside in hero; agent-e appends log lines to it
- `#wave-sim-root` — empty div in hero; agent c mounts the simulator into it
- `aside.activity-rail` — empty aside in hero; agent e appends log lines to it

## Shared CSS custom properties
_(fill when CSS declares variables that JS reads/writes)_
Expand All @@ -159,8 +159,8 @@ _(fill when frontend and backend are coupled)_

## Shared file structure
_(fill when multiple agents place files in a coordinated layout)_
- `src/components/Hero/Hero.tsx` — structure (agent-a)
- `src/components/Hero/hero.module.css` — styles (agent-b)
- `src/components/Hero/Hero.tsx` — structure (agent a)
- `src/components/Hero/hero.module.css` — styles (agent b)
````

### Injecting the contract into agent prompts
Expand Down Expand Up @@ -236,6 +236,16 @@ echo $ORCH_ID

### 6b. Create orchestration directory and state file

**Windows/Git Bash: align `TMPDIR` with the wmux sidebar first.** The wmux sidebar cockpit watches
Node's `os.tmpdir()` (`C:\Users\<you>\AppData\Local\Temp`), but in Git Bash `TMPDIR` is often unset,
so scripts fall back to `/tmp` — and the cockpit never sees the run. Export it before creating the
directory:

```bash
# Windows Git Bash only — point TMPDIR at the same temp dir the wmux app watches
export TMPDIR="$(cygpath "$LOCALAPPDATA")/Temp"
```

Create the directory:
```bash
mkdir -p "${TMPDIR:-/tmp}/wmux-orch-$ORCH_ID"
Expand All @@ -259,7 +269,7 @@ Write `state.json` using the Write tool. Schema:
"blockedBy": [],
"agents": [
{
"id": "agent-a",
"id": "a",
"label": "Subtask label",
"subtask": "Full subtask description",
"files": ["allowed/file/paths"],
Expand All @@ -269,7 +279,7 @@ Write `state.json` using the Write tool. Schema:
"status": "pending",
"exitCode": null,
"toolUses": 0,
"resultFile": "/tmp/wmux-orch-XXXXXX/agent-a-result.md",
"resultFile": "<orch-dir>/agent-a-result.md",
"startedAt": null,
"finishedAt": null
}
Expand All @@ -279,16 +289,28 @@ Write `state.json` using the Write tool. Schema:
"reviewer": {
"status": "pending",
"agentId": null,
"reportFile": "/tmp/wmux-orch-XXXXXX/review-report.md"
"reportFile": "<orch-dir>/review-report.md"
}
}
```

Use short agent IDs like "agent-a", "agent-b", etc. Set the first wave's status to "running", all others to "pending".
**Use BARE agent IDs: `"a"`, `"b"`, `"c"` — NOT `"agent-a"`.** Every script builds file paths by
prefixing `agent-` to the id (`spawn-agents.sh` looks for `agent-<id>-prompt.md`,
`collect-results.sh` for `agent-<id>-result.md`). With `"id": "a"` everything lines up as
`agent-a-prompt.md` / `agent-a-result.md`. With `"id": "agent-a"` the scripts look for
double-prefixed `agent-agent-a-prompt.md` — the launcher can't find the prompt, the pane silently
drops to a bare shell, and result collection misses the files.

**Use forward-slash paths in `state.json`** (`"cwd": "C:/projects/app"`, not `C:\projects\app`).
Backslashes written through a bash heredoc collapse into invalid JSON escapes (`\p`), every reader
fails to parse the file, and the sidebar silently freezes at 0/N.

Set the first wave's status to "running", all others to "pending".

### 6c. Generate agent prompt files

For EACH agent, create a prompt file at `{orch-dir}/agent-{id}-prompt.md` with:
For EACH agent, create a prompt file at `{orch-dir}/agent-{id}-prompt.md` — with the bare ids from
6b this is `agent-a-prompt.md`, `agent-b-prompt.md`, etc., exactly what `spawn-agents.sh` looks for:

```markdown
# Mission: [subtask label]
Expand Down Expand Up @@ -331,12 +353,23 @@ Use this format:
[Points of attention for other agents or reviewer]
```

If an agent's mission involves driving the wmux **browser panel** (web testing, SPA interaction,
form filling), append the contents of
`$PLUGIN_ROOT/skills/orchestrate/references/browser-driving.md` to its prompt — it documents the
CLI's sharp edges (eval scoping, ref format, framework-specific input recipes).

### 6d. Create wmux layout (if available)

**IMPORTANT: Work in the CURRENT workspace. Do NOT create or close workspaces — that hides agent panes from the user.**

The spawn script (`spawn-agents.sh`) automatically creates panes via `wmux split`.

**Pane hygiene — start each orchestration from a single coordinator pane.** Re-gridding a window
that already has extra panes (a previous run's agents, stray splits) does NOT reuse them: the old
panes' surfaces get orphaned as dead tabs on the coordinator pane. If leftover agent panes exist
from a previous wave or run, close them first (`wmux close-pane <paneId>` — positional id, and never
the coordinator's own pane).

### 6e. Spawn Wave 1 agents

**CRITICAL RULE: When wmux is available, you MUST use `wmux agent spawn` to create agents in visible terminal panes. Do NOT use Claude Code's `Agent` tool when wmux is available — the Agent tool creates invisible subagents that the user cannot see. The `Agent` tool is ONLY for degraded mode (no wmux).**
Expand Down Expand Up @@ -378,7 +411,7 @@ Spawn each agent using Claude Code's native Agent tool:

Before entering the loop, note how the user sees progress:

- **In wmux mode**: wmux's sidebar automatically watches `{TMPDIR}/wmux-orch-*/state.json` and renders a live cockpit (task, elapsed time, per-wave progress bars, per-agent state dots, tool counts). You don't need to do anything to keep it updated — the hooks already update state.json on tool-use and wave transitions, and the sidebar polls every second. Do NOT call the dashboard manually in wmux mode; it would be redundant and noisy.
- **In wmux mode**: wmux's sidebar automatically watches `{TMPDIR}/wmux-orch-*/state.json` and renders a live cockpit (task, elapsed time, per-wave progress bars, per-agent state dots, tool counts). `state.json` on disk is the ONLY channel to the cockpit — the app polls it every second (by mtime) and nothing pushes. Tool-use counts update via hooks running inside each agent's session, but **agent completion and wave/run transitions are YOUR job to write** (see the monitoring loop below). Do NOT call the text dashboard manually in wmux mode; it would be redundant and noisy.

- **In degraded mode (no wmux)**: you must print the text dashboard into Claude Code's conversation at each wave transition so the user can see progress. Run:
```bash
Expand All @@ -388,33 +421,70 @@ Before entering the loop, note how the user sees progress:

### With wmux (poll-based monitoring):

After spawning Wave N agents, enter a monitoring loop. Poll every 15-20 seconds:
**The completion signal is the RESULT FILE, not process exit.** `launch-agent.js` runs each agent's
Claude **interactively** (full TUI, no `-p`), so a finished agent idles at its prompt and never
exits — `wmux agent list` reports `"running"` forever. A loop that waits for `"exited"` never
terminates. Poll for the result files instead:

After spawning Wave N agents, enter a monitoring loop. Poll every 15-20 seconds for each agent's
result file:

```bash
wmux agent list
ls "[orch-dir]"/agent-*-result.md 2>/dev/null
```

For each agent, check the `"status"` field:
- `"running"` → agent is still working
- `"exited"` → agent has finished (check `"exitCode"`: 0 = success, non-zero = failure)
An agent is done when `[orch-dir]/agent-<id>-result.md` exists. (`wmux agent list` is still useful
to confirm agents *spawned* and to get their `agentId`/`surfaceId` — just not for completion.)

**While agents are running, report status to the user:**
**While agents are working, report status to the user:**
- Tell the user which agents are still working and which have finished
- Example: "Wave 1: Agent A (HTML+CSS) still running, Agent B (i18n) finished (exit 0)"
- Example: "Wave 1: Agent a (HTML+CSS) still working, Agent b (i18n) finished (result file written)"

**You own state.json.** Claude Code's Stop/SubagentStop hooks do NOT fire for wmux-spawned agents —
they are independent processes, not subagents of your session — so nothing updates `state.json`
automatically. As agents finish, update it yourself using the state helpers, **with the status
vocabulary the sidebar actually counts** (`exited` for agents, `complete` for waves/run — NOT
`completed`, which the sidebar ignores and leaves the cockpit stuck at 0/N):

```bash
source "$PLUGIN_ROOT/scripts/orchestration-state.sh"
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
update_agent "[orch-dir]" "<id>" "status=exited" "exitCode=0" "finishedAt=$NOW"
# when the whole wave is done:
update_state "[orch-dir]" ".waves[N].status" complete
```

**When ALL agents in the current wave show `"status": "exited"`:**
(Windows note: reads of `state.json` can race the app's 1-second poller — a read may intermittently
fail even though the file exists. Retry short reads a few times, and prefer one atomic final write
over many rapid updates.)

1. Read each agent's result file (if they created one):
**When ALL agents in the current wave have written their result files:**

1. Read each agent's result file:
```
[orch-dir]/agent-[id]-result.md
```
2. Report results to the user: which agents succeeded, which failed, what they produced
3. If there are more waves:
3. Mark the wave `complete` in state.json (see above), then **reap the idle agent TUIs**:
`wmux agent kill <agentId>` for each finished agent (ids from `wmux agent list`), and close their
panes (`wmux close-pane <paneId>`) so the next wave starts from a clean layout.
⚠ `agent kill` does NOT kill processes the agent started (dev servers, watchers) — if agents
launched servers, sweep the project's ports for orphaned listeners before starting new ones.
4. If there are more waves:
a. Generate prompt files for Wave N+1 (inject previous wave results into the "Previous Wave Results" section)
b. Spawn Wave N+1 agents: `bash "$PLUGIN_ROOT/scripts/spawn-agents.sh" "[orch-dir]" [N+1]`
c. Verify agents spawned with `wmux agent list`
d. Continue monitoring loop
4. If all waves are done, proceed to Phase 8
5. If all waves are done, mark the run `complete` (`update_state "[orch-dir]" ".status" complete`)
and proceed to Phase 8

**Nudging a running worker:** `wmux send` / `send-key` target the **caller's own surface** by
default — without `--surface` the text lands in YOUR session as fake input, not the worker's. To
redirect a worker mid-flight: write the instruction to a file, then
`wmux send --surface <workerSurfaceId> "read and follow <file>"` and
`wmux send-key Enter --surface <workerSurfaceId>` (surface ids are in state.json after spawn, or
`wmux agent list`). Verify delivery by its effects (result file, file changes) — `read-screen` may
be unavailable in some builds.

### Without wmux (degraded mode — Agent tool returns):
1. Wait for all Wave N agents to complete (their Agent tool calls return)
Expand Down Expand Up @@ -443,3 +513,11 @@ After the reviewer completes, present a summary:
- Test results (if reviewer ran tests)
- Reviewer findings and corrections
- Offer actions: **commit** / **view full diff** / **abort all changes**

Then **collapse the layout back to a single coordinator pane**: `wmux agent kill <agentId>` any
agents still idling, `wmux close-pane <paneId>` their panes, and sweep for orphaned child processes
(dev servers the agents started survive `agent kill` and can hold ports hostage for the next run).
A clean single-pane state is what makes the next orchestration's grid come up without orphaned tabs.

To abort a botched run and clear the cockpit: `update_state "[orch-dir]" ".status" aborted` — the
sidebar only tracks the most recent *running* orchestration.
86 changes: 86 additions & 0 deletions skills/orchestrate/references/browser-driving.md
CDC5
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Driving the wmux browser panel from the CLI

Field notes for agents that automate the wmux browser panel (`wmux browser …`). The panel is a real
Chromium webview the user watches live — everything you do is visible, which is the point. These are
the sharp edges that cost time when discovered by trial and error.

## Core workflow

```bash
wmux browser open <url> # navigate
wmux browser snapshot # accessibility tree with element refs (eN)
wmux browser click e45 # click element by ref
wmux browser type e3 "text" # type into element
wmux browser get-text # page text
wmux browser eval "<js>" # run JavaScript in the page
wmux browser screenshot # capture PNG
```

## Sharp edges

- **Refs take NO `@` prefix.** `wmux browser click e45` — passing `@e45` fails with
`Invalid parameters`. (Docs elsewhere may show `@eN`; the CLI wants the bare ref.)

- **Refs can go stale on SPAs.** Against client-rendered apps (React/Vue with re-renders), even a
fresh-from-snapshot ref may return `ref_not_found`. Reliable fallback: `wmux browser eval` with
DOM queries — `document.querySelectorAll('[role=radio]')[3].click()`. The user still sees every
action live in the panel.

- **`eval` shares ONE persistent JS scope across calls.** A second `const x = …` throws
`Uncaught SyntaxError` (redeclaration). Always wrap snippets in an IIFE:
`(() => { …; return 'result' })()`.

- **`reload` may be rejected by the app** even though CLI usage lists it — use
`wmux browser eval "location.reload()"` instead.

- **`snapshot` prints JSON** with the whole accessibility tree as a single `\n`-escaped string.
Don't grep it raw — parse it with a real JSON parser (node/python/jq), and write it to a file
first if it's large.

- **Shell quoting eats `$` in double-quoted eval one-liners.** `"…t.match(/\$611/)…"` gets `$6`
expanded as a positional parameter and silently matches nothing. Single-quote any eval snippet
containing `$` (money regexes!), or write the snippet to a file.

- **wmux binds a CDP endpoint on port 9222.** Don't point other CDP clients (e.g. a chrome-devtools
MCP configured for `127.0.0.1:9222`) at it during a session — whoever binds 9222 first wins, the
attachment target is nondeterministic, and teardown from another client can disrupt the wmux
window. Use `wmux browser …` for panel automation.

## Framework-specific input recipes

- **React controlled inputs** ignore a plain `.value =` assignment. Use the native setter, then
dispatch an input event:
```js
(() => {
const el = document.querySelector('#email');
const set = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
set.call(el, 'user@example.com');
el.dispatchEvent(new Event('input', { bubbles: true }));
return el.value;
})()
```
A node's React handlers/props are reachable under its `__reactProps$…` key when you need them.

- **Radix UI / shadcn primitives ignore synthetic `.click()` and `.focus()`.** Plain buttons and
links are fine with `.click()`; Radix primitives need real-looking event sequences:
- **Tabs**: dispatch `pointerdown → mousedown → pointerup → mouseup → click` (PointerEvent with
`pointerId: 1`) on the trigger. The `data-state` flip is **async** — verify it in a *later*
eval call, not the same snippet.
- **Tooltip**: gates on focus-visible; programmatic `.focus()` never opens it. Open via hover:
`pointerover → pointerenter → pointermove` with `pointerType: "mouse"`.
- **Select**: try the full pointer sequence on the trigger and then on the chosen
`[data-slot=select-item]` / `[role=option]` first — in some Radix versions that works. If it
does nothing, call the React handlers directly via `__reactProps$…`: `onPointerDown` on the
trigger to open (`{button: 0, ctrlKey: false, pointerType: 'mouse', target: trigger,
currentTarget: trigger, preventDefault(){}, stopPropagation(){}, defaultPrevented: false,
nativeEvent: {}}`), then `onKeyDown` with `{key: 'Enter', code: 'Enter', …}` on the option to
commit (its `onPointerUp` does NOT commit). Verify by re-reading the trigger's text in a later
eval.

## Don't trust eval-polling for transient UI

Toasts, animations, and other short-lived UI can appear perfectly to the human watching the panel
while repeated `eval`/`get-text` polls report them absent (the poll cadence misses the
add/remove window). If transient UI "looks broken" only under polling: ask the user what they see
in the panel, or re-test with a long duration (e.g. `toast('x', { duration: 60000 })`) before
concluding the app is broken.
4 changes: 4 additions & 0 deletions skills/wmux-detect/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ bash "$PLUGIN_ROOT/scripts/detect-wmux.sh"
- The orchestrator can use `wmux split`, `wmux agent spawn`, `wmux markdown set` etc.
- Full multi-pane visual experience is available

Detection works even when `wmux` isn't on PATH: shells spawned by wmux always carry `$WMUX_CLI`
(the path to the CLI script), and the detection script falls back to running it via `node` when the
bare command isn't found. The same fallback applies to every plugin script that calls `wmux`.

**If output is "unavailable":**
- wmux is not running or not installed
- Fall back to Claude Code's native `Agent` tool for parallel workers
Expand Down
0