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
8 changes: 8 additions & 0 deletions .claude/hooks/delegation-guard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ case "$rel_path" in
.claude/*|.agents/*|docs/*|.github/*) hook_allow ;;
esac

# Merge conflict resolution is mechanical arbitration, not new Rust
# implementation work. Limit the exception to the Rust file that is
# currently unmerged so unrelated implementation files stay guarded.
conflicted_paths="$(git -C "$HOOK_CWD" diff --name-only --diff-filter=U 2>/dev/null || true)"
if printf '%s\n' "$conflicted_paths" | grep -qxF "$rel_path"; then
hook_allow
fi

if [ "$role" = "root" ] || [ -z "$role" ]; then
hook_block "Root orchestrator should dispatch to a subagent for Rust implementation (file: $file_path). Use the 01-implementer agent."
fi
Expand Down
54 changes: 53 additions & 1 deletion .claude/hooks/tests/test_delegation_guard_subagent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,59 @@ for path in ".claude/hooks/role-marker.sh" ".agents/rules/foo.md" "docs/src/intr
done
pass "guard allows root edits of infra files (.claude/, .agents/, docs/, .github/)"

# --- Step 6: Backwards-compat — if a role file exists with
# --- Step 6: Merge conflict resolution is allowed from root. Conflict
# marker arbitration is mechanical, so the root orchestrator may edit
# Rust source files while the worktree has unmerged paths.
merge_repo="$TMP_DIR/merge-repo"
mkdir -p "$merge_repo/crates/plumb-core/src"
git -C "$merge_repo" init -q
git -C "$merge_repo" config user.name "Hook Test"
git -C "$merge_repo" config user.email "hook-test@example.com"
printf 'pub fn value() -> u8 { 1 }\n' > "$merge_repo/crates/plumb-core/src/lib.rs"
git -C "$merge_repo" add crates/plumb-core/src/lib.rs
git -C "$merge_repo" commit -qm "initial"
git -C "$merge_repo" checkout -qb feature
printf 'pub fn value() -> u8 { 2 }\n' > "$merge_repo/crates/plumb-core/src/lib.rs"
git -C "$merge_repo" commit -am "feature" -q
git -C "$merge_repo" checkout -q -
printf 'pub fn value() -> u8 { 3 }\n' > "$merge_repo/crates/plumb-core/src/lib.rs"
git -C "$merge_repo" commit -am "main" -q
git -C "$merge_repo" merge feature >/dev/null 2>&1 && fail "expected merge conflict"

merge_input="$(jq -n \
--arg sid "$session_id" \
--arg cwd "$merge_repo" \
--arg fp "$merge_repo/crates/plumb-core/src/lib.rs" \
'{session_id: $sid, cwd: $cwd, hook_event_name: "PreToolUse", tool_name: "Edit", tool_input: {file_path: $fp, content: "x"}}')"
guard_output="$(env -u CLAUDE_SESSION_ID \
CLAUDE_PROJECT_DIR="$merge_repo" \
bash "$HOOKS_DIR/delegation-guard.sh" <<<"$merge_input")"
decision="$(printf '%s' "$guard_output" | jq -r '.decision // empty' 2>/dev/null || true)"
[ "$decision" = "allow" ] || fail "guard should ALLOW root .rs edit during merge conflict resolution, got: $guard_output"
pass "guard allows root Rust edits while unmerged paths exist"

unrelated_merge_input="$(jq -n \
--arg sid "$session_id" \
--arg cwd "$merge_repo" \
--arg fp "$merge_repo/crates/plumb-cli/src/main.rs" \
'{session_id: $sid, cwd: $cwd, hook_event_name: "PreToolUse", tool_name: "Edit", tool_input: {file_path: $fp, content: "x"}}')"
guard_output="$(env -u CLAUDE_SESSION_ID \
CLAUDE_PROJECT_DIR="$merge_repo" \
bash "$HOOKS_DIR/delegation-guard.sh" <<<"$unrelated_merge_input")"
decision="$(printf '%s' "$guard_output" | jq -r '.decision // empty' 2>/dev/null || true)"
[ "$decision" = "block" ] || fail "guard should BLOCK unrelated root .rs edit during merge conflict resolution, got: $guard_output"
pass "guard keeps unrelated Rust files blocked during merge conflict resolution"

printf 'pub fn value() -> u8 { 2 }\n' > "$merge_repo/crates/plumb-core/src/lib.rs"
git -C "$merge_repo" add crates/plumb-core/src/lib.rs
guard_output="$(env -u CLAUDE_SESSION_ID \
CLAUDE_PROJECT_DIR="$merge_repo" \
bash "$HOOKS_DIR/delegation-guard.sh" <<<"$merge_input")"
decision="$(printf '%s' "$guard_output" | jq -r '.decision // empty' 2>/dev/null || true)"
[ "$decision" = "block" ] || fail "guard should BLOCK root .rs edit after conflict resolution, got: $guard_output"
pass "guard blocks root Rust edits after the conflict is resolved"

# --- Step 7: Backwards-compat — if a role file exists with
# subagent:<name>, guard still allows. This preserves the existing
# escape hatch for harnesses that DO populate agent_type at
# SessionStart (future Claude Code versions) or for manual override.
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@
.claude/worktrees/
.claude/memory/
.claude/settings.local.json
.claude-pr/
node_modules/
4 changes: 1 addition & 3 deletions crates/plumb-mcp/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,13 @@ Built on `rmcp 0.2.x` with the `#[tool_router]` + `#[tool]` +

## Adding a new tool

See `.agents/rules/mcp-tool-patterns.md`. Summary:
See `.agents/rules/mcp-tool-patterns.md` for the handoff path. Summary:

1. Add a `Deserialize + JsonSchema` struct for the tool's args.
2. Add a `#[tool(description = "…")]` async method on `PlumbServer`.
3. Add a protocol test case in `crates/plumb-cli/tests/mcp_stdio.rs`.
4. Update `docs/src/mcp.md` tool table.

Use the `09-mcp-tool-author` subagent for cookie-cutter execution.

## Depends on

- `plumb-core` (types; `test-fake` feature enabled so `lint_url` can
Expand Down