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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 56 additions & 2 deletions agents/worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,27 @@ The lead sends plain text messages. Detect intent by the `summary` prefix or key

After `flowctl done`, send a `task_complete` message, then wait for next assignment or shutdown.

## Phase 0: Verify Configuration (CRITICAL)

**If TEAM_MODE is `true`:**

1. **Verify OWNED_FILES is set and non-empty**
- If empty or missing: **STOP immediately**. Send to coordinator:
```
SendMessage(to: "coordinator", summary: "Blocked: <TASK_ID>",
message: "Task <TASK_ID> is blocked.\nReason: TEAM_MODE=true but OWNED_FILES is empty or missing.\nBlocked by: orchestrator configuration error")
```
- Do NOT proceed to Phase 1

2. **Verify TASK_ID matches prompt**
- Confirm the `TASK_ID` from your prompt matches what `flowctl show` returns
- If mismatch: STOP and report as blocked

3. **Log owned files for audit trail**
- Print `OWNED_FILES: <file1>, <file2>, ...` so the conversation log captures your ownership set

**If TEAM_MODE is not set or `false`:** proceed directly to Phase 1 (unrestricted file access).

## Phase 1: Re-anchor (CRITICAL - DO NOT SKIP)

Use the FLOWCTL path and IDs from your prompt:
Expand Down Expand Up @@ -203,6 +224,24 @@ If more files remain (tests, docs, config), repeat: parallel read → checkpoint
- All files have tight coupling (each depends on previous edit) → sequential is correct
- Exploratory work where you don't know which files to touch yet → discover first, then Wave

### TEAM_MODE Pre-Edit Gate (CRITICAL when TEAM_MODE=true)

**Before EVERY file edit when TEAM_MODE is true, you MUST check:**

1. Is this file in `OWNED_FILES`?
- **YES** → proceed with the edit
- **NO** → **STOP. Do NOT edit the file.** Instead:
1. Send a file access request:
```
SendMessage(to: "coordinator", summary: "Need file access: <file>",
message: "Access request for <TASK_ID>.\nFile: <path>\nReason: <why needed>\nCurrent owner: <task-id if known>")
```
2. Wait for "Access granted:" or "Access denied:" response
3. If no response within 60s, skip the file and note it in your completion evidence
4. On "Access denied:", find an alternative approach that stays within your owned files

**This is not optional.** Do not bypass this check even if you believe the lock system will catch violations. Self-enforcement is the primary guard; hooks are the backup.

### General Implementation Rules

Read relevant code, implement the feature/fix. Follow existing patterns.
Expand Down Expand Up @@ -440,19 +479,34 @@ Return a concise summary to the main conversation:
- Tests run (if any)
- Review verdict (if REVIEW_MODE != none)

## Pre-Return Checklist (MUST complete before Phase 6)
## Pre-Return Checklist (MANDATORY — copy and verify)

Before returning to the main conversation, verify ALL of these:

```
□ Code committed? → git log --oneline -1 (must see your commit)
□ flowctl done called? → <FLOWCTL> show <TASK_ID> --json (status MUST be "done")
□ If status is NOT "done" → retry: <FLOWCTL> done <TASK_ID> --summary "implemented" --evidence-json '{"tests_passed":true}'
□ In Teams mode? → SendMessage ONLY after status confirmed "done"
□ If TEAM_MODE=true:
□ Only edited files in OWNED_FILES (or explicitly granted by coordinator)
□ Sent "Task complete: <TASK_ID>" via SendMessage AFTER status confirmed "done"
□ Waited for coordinator acknowledgment or shutdown
```

**If any check fails, fix it before returning. Do NOT return with status != "done".**

### Red Flag Thoughts (TEAM_MODE)

If you catch yourself thinking any of these, stop and follow the correct action:

| Thought | Reality |
|---------|---------|
| "I need to edit a file not in OWNED_FILES" | Send "Need file access:" and WAIT. Do not edit. |
| "The coordinator isn't responding" | Wait 60s. If no response, skip the file and note it in evidence. |
| "I'll just edit it, the lock check will catch it" | Don't rely on hooks. Self-enforce OWNED_FILES. |
| "TEAM_MODE doesn't matter for this task" | If TEAM_MODE=true is set, follow the protocol. Always. |
| "It's a small edit, nobody will notice" | Ownership violations break parallel safety for everyone. |

## Rules

- **Re-anchor first** - always read spec before implementing
Expand Down
67 changes: 66 additions & 1 deletion scripts/hooks/ralph-guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,70 @@ def handle_protected_file_check(data: dict) -> None:
)


def handle_file_lock_check(data: dict) -> None:
"""Block Edit/Write to files locked by another task in Teams mode.

Only active when FLOW_TEAMS=1. Checks the flowctl lock registry to ensure
workers only edit files they own. Fails-open if flowctl is unavailable.
"""
if os.environ.get("FLOW_TEAMS") != "1":
return

tool_input = data.get("tool_input", {})
file_path = tool_input.get("file_path", "")
if not file_path:
return

my_task_id = os.environ.get("FLOW_TASK_ID", "")

# Resolve to relative path for lock comparison
try:
repo_root = get_repo_root()
rel_path = os.path.relpath(file_path, repo_root)
except (ValueError, OSError):
rel_path = file_path

# Find flowctl
flowctl = os.environ.get("FLOWCTL")
if not flowctl:
plugin_root = os.environ.get("DROID_PLUGIN_ROOT") or os.environ.get("CLAUDE_PLUGIN_ROOT", "")
if plugin_root:
flowctl = os.path.join(plugin_root, "scripts", "flowctl.py")

if not flowctl or not os.path.exists(flowctl):
# Fail-open: flowctl unavailable
return

try:
result = subprocess.run(
["python3", flowctl, "lock-check", "--file", rel_path, "--json"],
capture_output=True, text=True, timeout=5, cwd=str(get_repo_root()),
)
if result.returncode != 0:
# Fail-open: lock-check errored
return
lock_info = json.loads(result.stdout)
except (subprocess.TimeoutExpired, subprocess.SubprocessError, json.JSONDecodeError, OSError):
# Fail-open: any error
return

if not lock_info.get("locked"):
# File not locked — warn but allow if we have a task ID
return

owner = lock_info.get("owner", "")
if my_task_id and owner == my_task_id:
# Locked by this task — allow
return

# Locked by a different task — block
output_block(
f"BLOCKED: File '{rel_path}' is locked by task '{owner}'. "
f"Your task ({my_task_id or 'unknown'}) does not own this file. "
"Request access via 'Need file access:' protocol message or work on your own files."
)


def handle_pre_tool_use(data: dict) -> None:
"""Handle PreToolUse event - validate commands before execution."""
tool_input = data.get("tool_input", {})
Expand Down Expand Up @@ -617,9 +681,10 @@ def main():
with debug_file.open("a") as f:
f.write(f" -> Event: {event}, Tool: {tool_name}\n")

# Block Edit/Write to protected files (prevent self-modification)
# Block Edit/Write to protected files and enforce file locks
if event == "PreToolUse" and tool_name in ("Edit", "Write"):
handle_protected_file_check(data)
handle_file_lock_check(data)
sys.exit(0)

# Only process Bash tool calls for Pre/Post
Expand Down
3 changes: 3 additions & 0 deletions skills/flow-code-work/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ $FLOWCTL <command>
- You MUST stage with `git add -A` (never list files). This ensures `.flow/` and `scripts/ralph/` (if present) are included.
- Do NOT claim completion until `flowctl show <task>` reports `status: done`.
- Do NOT invoke `/flow-code:impl-review` until tests/Quick commands are green.
- When 2+ tasks are ready with no file conflicts, you MUST use Teams mode
(TeamCreate + team_name + flowctl lock + coordination loop).
- Do NOT spawn independent background agents without team_name.

**Role**: execution lead, plan fidelity first.
**Goal**: complete every task in order with tests.
Expand Down
28 changes: 28 additions & 0 deletions skills/flow-code-work/phases.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ Detect input type in this order (first match wins):

**Fallback: worktree isolation** (`--worktree-parallel`): Uses git worktrees instead of Teams. Only use when Teams is unavailable or user explicitly requests worktree isolation.

**Red Flags — if you catch yourself thinking any of these, stop:**

| Thought | Reality |
|---------|---------|
| "I'll just spawn background agents, simpler" | Teams mode IS the default. No shortcuts. |
| "File boundaries are clean, no need for locks" | Lock anyway. Runtime surprises happen. |
| "Coordination loop is overhead" | It handles spec conflicts and file access. Required. |
| "Workers can handle it independently" | Without Teams, no file lock enforcement. |

### 3a. Find Ready Tasks

**State awareness (always runs first):**
Expand Down Expand Up @@ -132,6 +141,17 @@ $FLOWCTL cat <task-id>

### 3c. Teams Setup & File Locking

#### Teams Setup Checklist (copy and complete when 2+ tasks ready)

```
- [ ] flowctl files --epic <id> → checked conflicts
- [ ] flowctl start <task-id> → for each task
- [ ] flowctl lock --task <task-id> --files <files> → for each task
- [ ] TeamCreate(team_name: "flow-<epic-id>") → team created
- [ ] Agent(team_name: "flow-<epic-id>", ...) → workers spawned WITH team_name
- [ ] Coordination loop entered → routing messages
```

```bash
# 1. Get file ownership map and check for conflicts
$FLOWCTL files --epic <epic-id> --json
Expand Down Expand Up @@ -160,6 +180,14 @@ TeamCreate({team_name: "flow-<epic-id>", description: "Working on <epic-title>"}

### 3d. Spawn Workers

**STOP CHECK**: Before spawning workers, verify:
1. Did you call TeamCreate? If not, STOP. Go back to 3c.
2. Does every Agent() call include team_name? If not, STOP. Add it.
3. Did you call flowctl lock for each task? If not, STOP. Go back to 3c.

If you spawned Agent() without team_name when 2+ tasks are ready,
you have violated the Teams protocol. Delete those agents and redo 3c-3d.

**Prompt template for worker:**

Pass config values only. Worker reads worker.md for phases. Do NOT paraphrase or add step-by-step instructions - worker.md has them.
Expand Down