Patterns and protocols for effective collaboration using the driver/worker model.
- Check context first -- always run
get_contextat session start - Communicate findings -- agents can't see each other's work unless you send messages
- Report progress -- workers must heartbeat every 60-90s and report progress every 2-3 min
- Update task status -- keep the shared task list current
- Obey cancellation -- stop immediately when you see a STOP signal
The driver creates and assigns work, monitors progress, and intervenes when things go wrong.
Key tools:
create_taskwithassigned_to='any'for auto-assignment to workersworker_statusto see live progress, SLA status, and process activitycancel_agentto stop stuck workerssend_messageto give instructions or feedback
When Claude Code is configured as the driver (driver: claude-code in config):
get_session_context for 'claude-code'
set_presence agent='claude-code' status='working' workspace='/path/to/project'
create_task title='Add auth middleware' assigned_to='any' created_by='claude-code'
worker_status
cancel_agent agent='codex' cancelled_by='claude-code' reason='taking too long'
Use /pair-drive in Claude Code for a guided driver session.
Workers claim tasks, do the work, and report back.
Key tools:
claim_nextto get the next available taskheartbeatevery 60-90 seconds with progress inforeport_progressevery 2-3 minutes with task_id, description, percent_completesend_messageto report findings to the driverupdate_taskto mark work as completed
Any MCP client can join by calling register_agent. Once registered, it uses the same tools as built-in agents.
Claude Code and Cursor get the "MANDATORY progress-reporting" rules injected
via editor hooks (SessionStart/UserPromptSubmit/Stop for Claude Code;
sessionStart for the Cursor plugin), on top of whatever's in CLAUDE.md /
.mdc rule files. Before this existed, those hooks fired unconditionally for
any session with a live state.sqlite — including driver sessions, which
don't need "call heartbeat every 60-90s" reminders since they orchestrate
rather than execute tasks.
Each hook is a thin shim that delegates to:
mcp-stringwork hooks emit --event <session_start|user_prompt|stop|spawn> --platform <name>hooks emit resolves a role for the current session and either prints
the matching rule text or nothing at all:
| Signal | Resolves to |
|---|---|
STRINGWORK_AGENT env var set |
worker — Stringwork sets this on every spawned worker process, so this is the most reliable signal regardless of config |
orchestration.driver: auto, or orchestration.driver equals --platform |
driver |
| Neither of the above | worker — e.g. Claude Code hooks firing while orchestration.driver: cursor (a human running Claude Code as a manual worker) |
No orchestration configured at all |
legacy — always inject, matching pre-hooks-config behavior |
Defaults: driver sessions are silent, worker/legacy sessions get the
full mandatory rules (session_start/user_prompt/stop) or the spawn-prompt
{worker_rules} placeholder (Codex/Gemini, expanded in
orchestration.workers[].command). Override any (platform, role, event)
combination under a hooks: block in ~/.config/stringwork/config.yaml —
see the commented example in mcp/config.yaml. A
master kill switch (hooks.enabled: false) disables all injection outright.
The one thing you need to get right for the default to work correctly:
orchestration.driver must actually reflect who's driving. If you drive
directly from Claude Code with no Cursor open, set driver: auto or
driver: claude-code — otherwise Claude Code still resolves as a worker
(since it isn't the configured driver) and keeps getting the reminders.
Re-run ./scripts/install-claude-hooks.sh after upgrading Stringwork to pick
up shim changes; ./scripts/uninstall-claude-hooks.sh removes only
Stringwork's own hook entries, leaving any other hooks you've configured
(rtk, formatters, etc.) untouched.
get_context for '<your-agent-name>'
set_presence agent='<you>' status='working' workspace='/path/to/project'
read_messages for '<you>'
list_tasks assigned_to='<you>'
create_task title='Add auth middleware' assigned_to='any' created_by='cursor'
Using assigned_to='any' lets the server auto-assign to an available worker. Alternatively, assign directly: assigned_to='claude-code'.
claim_next agent='claude-code' # or: update_task id=X status='in_progress'
Do the work using native tools (file edit, search, git, terminal).
Heartbeat -- every 60-90 seconds:
heartbeat agent='claude-code' progress='Implementing auth middleware' step=2 total_steps=5
On your first heartbeat, include session_id (your CLI session/conversation ID) so the server can resume your session if you get restarted:
heartbeat agent='claude-code' progress='starting work' session_id='YOUR_CLI_SESSION_ID'
Structured progress -- every 2-3 minutes:
report_progress agent='claude-code' task_id=5 description='Auth middleware done, writing tests' percent_complete=60 eta_seconds=180
What happens if you don't report:
| Silence duration | Consequence |
|---|---|
| 4 minutes | Warning sent to driver |
| 7 minutes | Critical alert sent to driver, auto-cancellation imminent |
| 14 minutes (no heartbeat) | Worker auto-cancelled, output captured, task reset to pending |
send_message from='claude-code' to='cursor' content='
## Task #5 Complete: Add auth middleware
**Changes:**
- internal/middleware/auth.go -- JWT validation middleware
- internal/auth/service.go -- Token generation
**Testing:** All tests passing
**Notes:** Consider adding rate limiting next
'
update_task id=5 status='completed' updated_by='claude-code'
The driver sees a completion notification via piggyback banner on their next tool call.
worker_status
Shows each worker's: agent progress (heartbeat info), task progress (description, percent, SLA), process activity (runtime, output), and worktree info.
cancel_agent agent='claude-code' cancelled_by='cursor' reason='taking too long'
This does four things atomically:
- Captures the worker's recent output and stores it in the task's work context
- Cancels all in-progress tasks for the agent
- Sends a STOP message to the agent
- Kills the spawned worker process
When the worker is respawned (via replay_task, new task assignment, or unread messages), the server injects both the stored CLI session ID (for conversation resume) and the previous worker's output (so no information is lost). The replacement worker sees the captured output prominently in its prompt and can continue where the previous worker left off.
create_task title='Code review' assigned_to='any' created_by='cursor' expected_duration_seconds=300
If the task exceeds its SLA, the driver gets an alert.
create_task title='Fix auth bug' assigned_to='any' created_by='cursor' relevant_files='["internal/auth/handler.go","internal/auth/service.go"]' background='JWT tokens expire too quickly' constraints='["Do not change the token format"]'
Workers can retrieve this context with get_work_context. Constraints are surfaced prominently in get_work_context and claim_next responses with warning banners. Workers must obey constraints when present (e.g. "read-only" prevents file modifications). The server also injects constraint compliance rules into worker system prompts and spawn commands.
For hands-off operation:
claim_next agent='claude-code' dry_run=true # peek at what's next
claim_next agent='claude-code' # claim it
# ... do the work, report progress ...
update_task id=X status='completed' # or handoff
# repeat
handoff from='claude-code' to='cursor' summary='Auth middleware implemented' next_steps='Wire into router and add rate limiting'
When the driver cancels your work, you'll see on your next tool call:
STOP: 1 of your task(s) have been cancelled. Stop immediately, call read_messages, and exit.
You must:
- Stop all current work immediately
- Call
read_messagesto understand why - Exit cleanly -- do NOT continue working on cancelled tasks
Driver distributes tasks:
create_task title='Frontend auth UI' assigned_to='claude-code' created_by='cursor'
create_task title='Backend auth API' assigned_to='codex' created_by='cursor'
Both workers execute in parallel. Driver monitors via worker_status.
request_review from='cursor' to='claude-code' files='["internal/auth/handler.go"]' description='Review JWT handling for security issues'
The reviewer sends findings via send_message.
For complex features:
create_plan id='auth' title='Auth feature' goal='JWT auth with rate limiting' created_by='cursor'
update_plan action='add_item' id='1' title='JWT middleware' owner='claude-code'
update_plan action='add_item' id='2' title='Rate limiter' owner='codex'
update_plan action='add_item' id='3' title='Integration tests' owner='cursor' dependencies='["1","2"]'
Workers update their items as they progress:
update_plan action='update_item' item_id='1' status='completed' add_note='Implemented with RS256'
Check progress: get_plan id='auth'
send_message from='claude-code' to='cursor' content='
## Blocked on Task #10
Missing database schema definition. Should I create db/migrations/003_users.sql?
'
update_task id=10 status='blocked' blocked_by='Waiting for schema decision' updated_by='claude-code'
Driver unblocks:
send_message from='cursor' to='claude-code' content='Yes, follow the pattern in 001_initial.sql'
update_task id=10 status='in_progress' blocked_by='' updated_by='cursor'
Every tool response includes a banner when you have unread content:
You have 2 unread message(s) and 1 pending task(s). Call read_messages or get_session_context.
This means agents discover new messages on their very next tool call without polling.
If the driver cancels a worker, the piggyback is replaced with:
STOP: 1 of your task(s) have been cancelled. Stop immediately.
When an agent has unread messages and isn't the currently connected client, the server can spawn a command to wake them. Configured in config.yaml:
auto_respond:
claude-code:
command: ["claude", "--continue", "-p", "/pair-respond", "--dangerously-skip-permissions"]
cooldown_seconds: 30The server can push notifications/pair_update when the connected agent has new content:
{
"jsonrpc": "2.0",
"method": "notifications/pair_update",
"params": { "unread_messages": 2, "pending_tasks": 1 }
}- Check
get_contextat session start - Set
workspaceinset_presencewhen starting or switching projects - Report progress continuously (heartbeat + report_progress)
- Send detailed messages with file paths and line numbers
- Create tasks for follow-up work
- Ask questions when blocked
- Obey STOP signals immediately
- Work without checking context first
- Stay silent for more than 2 minutes while working
- Assume your pair can see your work -- communicate explicitly
- Create duplicate tasks -- check existing tasks first
- Ignore STOP signals -- stop immediately
- Make big architectural decisions without discussing first
## Task #X Complete: [title]
**Summary:** [1-2 sentence overview]
**Changes:**
- [file:line] - description
**Testing:** [how you verified]
**Next steps:** [follow-up needed]
## Blocked on Task #X: [reason]
**Problem:** [description]
**Tried:** [what you attempted]
**Need:** [what would unblock you]
## Handing off: [feature]
**Completed:** [done items]
**Remaining:** [todo items]
**Context:** [important background]
- QUICK_REFERENCE.md -- tool usage examples
- SETUP_GUIDE.md -- installation and configuration
- ARCHITECTURE.md -- system design