From b53e4490cac73d53a7902317d8254c891a474d53 Mon Sep 17 00:00:00 2001 From: Bobby Date: Wed, 13 May 2026 05:57:30 -0700 Subject: [PATCH] fix(bootstrap_gate): exempt pact-secretary spawn from PreToolUse block Resolves a fresh-session bootstrap deadlock where every Agent dispatch is blocked because secretary is not yet in members[], but the only call that adds secretary to members[] is itself an Agent dispatch. ## Reproduction (PACT 4.1.10, fresh team) 1. TeamCreate adds only team-lead to ~/.claude/teams//config.json members[]. 2. On the next UserPromptSubmit, bootstrap_marker_writer.py fires but sees no secretary in members[]. Per _team_has_secretary it suppresses output and does NOT write the bootstrap-complete marker. 3. Subsequent Agent tool calls hit bootstrap_gate.py, which checks for the marker file. No marker -> block (PACT bootstrap required). 4. The only way to add secretary to members[] is to spawn pact-secretary via Agent. But Agent is in _BLOCKED_TOOLS unconditionally. Deadlock. Sessions that REUSE an existing team config never hit this because secretary persists in members[] from the prior session. Every FRESH team creation deadlocks until the user manually edits config.json to inject a stub secretary entry (the workaround documented in 2026-05-09 / pact-6d6cf61a). ## The fix Narrow exempt path in _check_tool_allowed for Agent calls where subagent_type == pact-secretary (or the colon-prefixed PACT:pact-secretary form that callers often use). All other Agent dispatch remains gated. ## Precedent Adjacent secretary carve-outs already shipped in: - #716 (teachback gate) - secretary exempt from teachback enforcement - #682 (self-completion) - secretary exempt from self-completion block This PR adds the missing carve-out in the PreToolUse gate, completing the pattern. ## Verified Patched runtime cache at the PACT 4.1.10 location on 2026-05-13 and successfully spawned pact-secretary via Agent(subagent_type=PACT:pact-secretary) without the marker file present. Confirms the exempt path slips the bootstrap-resolving spawn through; all other Agent calls continue gated. --- pact-plugin/hooks/bootstrap_gate.py | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pact-plugin/hooks/bootstrap_gate.py b/pact-plugin/hooks/bootstrap_gate.py index 90d3abe5..09dd2d18 100755 --- a/pact-plugin/hooks/bootstrap_gate.py +++ b/pact-plugin/hooks/bootstrap_gate.py @@ -290,6 +290,36 @@ def _check_tool_allowed(input_data: dict) -> str | None: if isinstance(tool_name, str) and tool_name.startswith("mcp__"): return None + # Bootstrap-resolving Agent dispatch exempt from PreToolUse block. + # + # The pact-secretary spawn is the call that ADDS secretary to the team + # config's members[] array, which is the load-bearing pre-condition for + # bootstrap_marker_writer.py to write the bootstrap-complete marker. + # Without this exempt path, fresh-session bootstrap deadlocks: + # + # 1. TeamCreate adds only team-lead to members[] + # 2. bootstrap_marker_writer requires `secretary` in members[] before + # it will write the marker (see _team_has_secretary) + # 3. This gate blocks all Agent dispatch until the marker exists + # 4. The Agent dispatch that would ADD secretary to members[] is the + # pact-secretary spawn — but it's blocked by step 3 + # + # The deadlock affects every fresh team creation. Sessions that REUSE + # an existing team config never hit it because secretary persists in + # members[] from the prior session. First documented in PACT 4.1.5 + # (2026-05-09) and reproduced in PACT 4.1.10 (this fix). Adjacent + # carve-outs already shipped for secretary in #716 (teachback gate) + # and #682 (self-completion); this PR adds the missing carve-out in + # the PreToolUse gate. + # + # Both colon-prefixed ("PACT:pact-secretary") and bare ("pact-secretary") + # subagent_type forms are accepted because the Claude Code platform may + # resolve either depending on plugin namespace context. + if tool_name == "Agent": + sub = input_data.get("tool_input", {}).get("subagent_type", "") + if sub in ("PACT:pact-secretary", "pact-secretary"): + return None + # Blocked implementation tools # frozenset membership is type-safe — no isinstance guard needed if tool_name in _BLOCKED_TOOLS: