name: monitor-patterns description: "Monitor primitive usage patterns for streaming command output as discrete events. Provides decision matrix (Monitor vs run_in_background vs raw Bash), canonical helper signatures (monitor_turbo_stream, monitor_vercel_deploy, monitor_gh_ci, monitor_gh_comments, monitor_lock_files), before/after migration patterns, and anti-patterns. Use when running long-lived commands that produce per-line events — Playwright suites, Vercel deploys, CI runs, PR bot comments, file-watch signals — and you need to react per-event instead of waiting for batch completion. Triggers: Playwright test execution, deploy state watching, CI completion polling, bot review polling, multi-line stdout streaming." user-invocable: false allowed-tools: Read, Glob, Grep, Bash
Monitor Patterns
The Monitor primitive wraps a shell command and treats each stdout line as an event notification. Use it when the problem is "tell me every time X happens" — not "wait until X is done" (that's run_in_background: true).
Every Monitor invocation MUST use a selective filter (grep --line-buffered, jq, awk, or a helper function) so only discrete events reach the chat. Raw log tails auto-stop.
Decision Matrix
| Need | Primitive | Why |
|---|---|---|
| Run a Playwright suite, react to each PASS/FAIL/SKIP | Monitor + filter | Per-test events, no buffering, low context cost |
| Wait for one test run to finish (no streaming) | Bash({ run_in_background: true }) |
Single completion signal, no per-event work |
| Tail Vercel deploy state during CI/E2E setup | Monitor + monitor_vercel_deploy |
Discrete READY/ERROR transitions |
| Watch CI for terminal state | Monitor + monitor_gh_ci |
Final-state-only event stream |
| Watch PR for bot review comments | Monitor + monitor_gh_comments |
Per-comment events |
| Watch a cooperative signal directory | Monitor + monitor_lock_files |
inotify + ls fallback |
One-shot diagnostic command (playwright test --list, bd show) |
Raw Bash |
No streaming needed |
Native blocking command with clean exit (gh run watch --exit-status) |
Raw Bash |
Already does the right thing |
Canonical Helpers
Source the library in any command or agent — never reinvent poll loops:
source ~/dev/cc/scripts/lib/monitor-helpers.sh
| Helper | Signature | Purpose |
|---|---|---|
monitor_turbo_stream |
TURBO_ARGS... |
Per-package / per-spec completion lines from a turbo or test run |
monitor_vercel_deploy |
PROJECT BRANCH [POLL_SECONDS] |
READY / ERROR transitions on a deployment |
monitor_gh_ci |
BRANCH [POLL_SECONDS] |
CI terminal state for a branch |
monitor_gh_comments |
PR_NUMBER BOT_LOGIN [POLL_SECONDS] |
Stream bot review comments as they post |
monitor_lock_files |
LOCK_DIR PATTERN_REGEX |
inotify-based file-watch with ls fallback |
Defaults and edge cases live inside the helpers. Read ~/dev/cc/scripts/lib/monitor-helpers.sh for full signatures.
Three Canonical Patterns
1. Filtered-stream — wrap a streaming tool, grep for terminal lines
The most common e2e/test pattern. Replaces raw pnpm playwright test invocations.
Before (raw Bash — blocks, dumps all stdout):
cd packages/e2e && pnpm with-env playwright test "$TEST_PATH" 2>&1
After (Monitor — per-spec completion events):
source ~/dev/cc/scripts/lib/monitor-helpers.sh
monitor_turbo_stream pnpm --filter @oo/e2e playwright test "$TEST_PATH" --reporter=line
The agent sees [wave-2] ✓ tests/foo.spec.ts:42 (1.2s) per pass and [wave-2] ✗ tests/bar.spec.ts:18 per fail — each line is one event, not a buffered dump.
2. Shell-poll — internal loop polling a remote API
Used for cloud state where there's no native streaming. Helpers like monitor_vercel_deploy and monitor_gh_ci implement this internally.
source ~/dev/cc/scripts/lib/monitor-helpers.sh
monitor_vercel_deploy otaku-odyssey dev 10 # poll every 10s, emit READY/ERROR
The wrapped helper polls the Vercel API and emits one line per state transition. The agent reacts to the READY event by triggering downstream work, or to ERROR by surfacing the failure.
3. File-watch — inotifywait -m on a cooperative signal dir
Used when commands write completion markers to a shared directory.
source ~/dev/cc/scripts/lib/monitor-helpers.sh
monitor_lock_files "/tmp/wave-locks" '^wave-[0-9]+\.done$'
Each new lock file matching the regex emits one event. Used by /plan:strategy Phase 3 fan-out gate.
Poll-Loop Exit Anti-Pattern: never gate on a strict parser whose error is swallowed
A shell-poll helper (Pattern 2) breaks out of its while loop when a status field reaches a terminal value. If the variable that gates the break/return is extracted with a strict parser (jq) whose error is swallowed (2>/dev/null), the loop silently never exits — it runs to its iteration cap (tens of minutes) instead of stopping on completion.
Root cause (observed against Azure DevOps): ADO build / timeline JSON embeds raw control chars (U+0000–U+001F) inside string fields — the triggering commit message, triggerInfo, log URLs, and issue messages. jq is strict and errors on unescaped control chars. With ... | jq -r '.status' 2>/dev/null the error is discarded, the gating variable becomes empty (""), the terminal-state comparison never matches, and the loop polls forever.
Tell-tale signature: every progress line renders the gated field as empty (e.g. / :: where a status should be), and the stream ends with no terminal (READY/succeeded/FAILED) line — the Monitor lifetime expires instead.
Fix — extract the gate scalar robustly. Two idioms, in order of preference:
# A. grep-extract the one scalar the loop gates on (control-char-proof, no parser)
bstatus=$(printf '%s' "$body" | grep -o '"status":"[^"]*"' | head -1 | cut -d'"' -f4)
# B. python json.loads(strict=False) — when you need structured fields, not just one scalar
state=$(printf '%s' "$body" | python3 -c \
'import sys,json; d=json.load(sys.stdin, strict=False); print(d.get("result") or d.get("status") or "")')
Rules:
- NEVER pipe raw ADO (or any control-char-bearing) JSON through
jqto produce the variable a loop's exit depends on. - NEVER
2>/dev/null-swallow the parser whose output gates the loop — a swallowed failure on the gate scalar is an infinite loop. (Swallowing is fine on a parser that only formats an event line; the loop's exit does not depend on it.) - The robust-extract idiom belongs in the helper, not the caller — see
monitor_azure_pipelinein~/dev/cc/scripts/lib/monitor-helpers.sh, which gates its terminal-statereturnon apython json.loads(strict=False)-extractedstate(idiom B — it pulls the precise run fields.[0].result/.id/._links.web.href, control-char-proof) so it cannot hang on ajqparse error.
When NOT to Use Monitor
- One-shot lookups (
playwright test --list,bd show,git log,gh pr view) — Monitor's per-line semantics add no value; raw Bash with stdout capture is correct - Native-blocking commands with clean exit codes (
gh run watch --exit-status) — already does the right thing; wrapping in Monitor adds latency without benefit - Parallel
Agentdispatches — Monitor cannot observe sub-agent completion; the orchestrator coordinates fan-in via task results, not stdout streaming - Non-streaming commands — anything that produces output as one batch at the end (e.g. a curl response) —
run_in_background: trueis the right primitive
Reference Sites
Production usage to copy from:
| Command / Agent | Monitor usage |
|---|---|
/p2p Step 3 bot resolvers |
monitor_gh_comments per bot login |
/apply:all Step 4.7 deploy gate |
monitor_vercel_deploy (Vercel) |
/test:test Step 4d CI poll |
monitor_gh_ci |
/plan:strategy Phase 3 fan-out |
monitor_lock_files |
/test:run-quality-gates |
monitor_turbo_stream per gate |
/test:e2e Phase 11 triage dispatch |
Agent fan-out (NOT Monitor — sub-agent completion not observable via stdout) |
Cross-Reference
Full primitive specification in ~/dev/cc/rules/TOOLING.md § Monitor Tool Pattern. This skill is the agent-facing summary — the rules file is the canonical contract.