diff --git a/agents/worker.md b/agents/worker.md index d9670db5..2c12a7be 100644 --- a/agents/worker.md +++ b/agents/worker.md @@ -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: ", + message: "Task 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: , , ...` 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: @@ -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: ", + message: "Access request for .\nFile: \nReason: \nCurrent owner: ") + ``` + 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. @@ -440,7 +479,7 @@ 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: @@ -448,11 +487,26 @@ Before returning to the main conversation, verify ALL of these: □ Code committed? → git log --oneline -1 (must see your commit) □ flowctl done called? → show --json (status MUST be "done") □ If status is NOT "done" → retry: done --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: " 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 diff --git a/scripts/hooks/ralph-guard.py b/scripts/hooks/ralph-guard.py index d2f647d7..6e731fe3 100755 --- a/scripts/hooks/ralph-guard.py +++ b/scripts/hooks/ralph-guard.py @@ -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", {}) @@ -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 diff --git a/skills/flow-code-work/SKILL.md b/skills/flow-code-work/SKILL.md index 5647eda5..6c44213d 100644 --- a/skills/flow-code-work/SKILL.md +++ b/skills/flow-code-work/SKILL.md @@ -23,6 +23,9 @@ $FLOWCTL - 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 ` 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. diff --git a/skills/flow-code-work/phases.md b/skills/flow-code-work/phases.md index 146e2faf..c15dc31e 100644 --- a/skills/flow-code-work/phases.md +++ b/skills/flow-code-work/phases.md @@ -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):** @@ -132,6 +141,17 @@ $FLOWCTL cat ### 3c. Teams Setup & File Locking +#### Teams Setup Checklist (copy and complete when 2+ tasks ready) + +``` +- [ ] flowctl files --epic → checked conflicts +- [ ] flowctl start → for each task +- [ ] flowctl lock --task --files → for each task +- [ ] TeamCreate(team_name: "flow-") → team created +- [ ] Agent(team_name: "flow-", ...) → workers spawned WITH team_name +- [ ] Coordination loop entered → routing messages +``` + ```bash # 1. Get file ownership map and check for conflicts $FLOWCTL files --epic --json @@ -160,6 +180,14 @@ TeamCreate({team_name: "flow-", description: "Working on "} ### 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.