Skip to content

Commit 69151a0

Browse files
committed
feat(plugin): update council handoff, stage, and blocker state from PostToolUse (#1368)
Add council lifecycle model to on_tool_end() so PostToolUse maintains meaningful state transitions for the request-driven council UX. - Add forward-only stage advancement heuristic (opening→reviewing→consensus→done) triggered by tool completion patterns (specialist tools, edit tools, parse_mode) - Add blocker count detection from quality-check Bash output (pytest, tsc, eslint) - Guard read_hud_state behind tool_name check to avoid unnecessary disk I/O - Use specific lint patterns (yarn lint, npm run lint) to prevent false positives - Add 43 new tests (107 total) covering stage transitions, blocker detection, integration, and full request lifecycle Closes #1368
1 parent ab22313 commit 69151a0

2 files changed

Lines changed: 459 additions & 3 deletions

File tree

packages/claude-code-plugin/hooks/lib/hud_helpers.py

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,35 @@
1616

1717
import json
1818
import os
19+
import re
1920
from pathlib import Path
2021
from typing import Dict, List, Optional
2122

22-
from hud_state import update_hud_state
23+
from hud_state import read_hud_state, update_hud_state
24+
25+
# Council lifecycle stages — forward-only progression (#1368)
26+
COUNCIL_STAGES = ("opening", "reviewing", "consensus", "done")
27+
28+
# Tool completions that signal specialist analysis is underway
29+
_SPECIALIST_SIGNAL_TOOLS = frozenset({
30+
"Agent",
31+
"mcp__codingbuddy__analyze_task",
32+
"mcp__codingbuddy__prepare_parallel_agents",
33+
"mcp__codingbuddy__dispatch_agents",
34+
"mcp__codingbuddy__generate_checklist",
35+
})
36+
37+
# Tool completions that signal the council is converging on decisions
38+
_CONSENSUS_SIGNAL_TOOLS = frozenset({
39+
"Edit",
40+
"Write",
41+
"mcp__codingbuddy__update_context",
42+
})
43+
44+
# Quality-check command patterns for blocker detection
45+
_TEST_CMD_PATTERNS = ("pytest", "vitest", "jest", "yarn test", "npm test")
46+
_TYPECHECK_CMD_PATTERNS = ("tsc", "type-check", "typecheck")
47+
_LINT_CMD_PATTERNS = ("eslint", "prettier --check", "yarn lint", "npm run lint")
2348

2449
_DEFAULT_PLUGINS_FILE = str(
2550
Path.home() / ".claude" / "plugins" / "installed_plugins.json"
@@ -164,8 +189,9 @@ def on_tool_end(
164189
) -> None:
165190
"""Record stable post-action state after a tool completes.
166191
167-
Called from PostToolUse. Captures agent handoffs and phase
168-
transitions that are evident from tool outputs.
192+
Called from PostToolUse. Captures agent handoffs, phase
193+
transitions, council stage advancement, and blocker counts
194+
evident from tool outputs (#1368).
169195
170196
Args:
171197
tool_name: Name of the completed tool.
@@ -190,6 +216,25 @@ def on_tool_end(
190216
updates["currentMode"] = mode
191217
updates["phase"] = phase
192218

219+
# Council stage advancement (#1368) — only read state when the
220+
# tool could actually trigger a transition, avoiding disk I/O
221+
# on every Read/Grep/Glob call.
222+
_council_tools = _SPECIALIST_SIGNAL_TOOLS | _CONSENSUS_SIGNAL_TOOLS | {"mcp__codingbuddy__parse_mode"}
223+
if tool_name in _council_tools:
224+
sf_kwargs = {"state_file": state_file} if state_file else {}
225+
state = read_hud_state(fill_defaults=True, **sf_kwargs)
226+
227+
if state.get("councilActive"):
228+
current_stage = state.get("councilStage", "")
229+
next_stage = _infer_council_advance(tool_name, current_stage)
230+
if next_stage:
231+
updates["councilStage"] = next_stage
232+
233+
# Blocker detection (#1368)
234+
blocker_count = _detect_blocker_count(tool_name, tool_input, tool_output)
235+
if blocker_count is not None:
236+
updates["blockerCount"] = blocker_count
237+
193238
if updates:
194239
if state_file:
195240
update_hud_state(state_file=state_file, **updates)
@@ -307,6 +352,80 @@ def on_council_update(
307352
# ---- private helpers ----
308353

309354

355+
def _infer_council_advance(
356+
tool_name: str,
357+
current_stage: str,
358+
) -> Optional[str]:
359+
"""Infer the next council stage from a completed tool.
360+
361+
Stage transitions are forward-only:
362+
opening → reviewing → consensus → done
363+
364+
Returns the new stage name, or None if no transition applies.
365+
"""
366+
if not current_stage or current_stage not in COUNCIL_STAGES:
367+
return None
368+
369+
if current_stage == "done":
370+
return None
371+
372+
if current_stage == "opening" and tool_name in _SPECIALIST_SIGNAL_TOOLS:
373+
return "reviewing"
374+
375+
if current_stage == "reviewing" and tool_name in _CONSENSUS_SIGNAL_TOOLS:
376+
return "consensus"
377+
378+
if current_stage == "consensus" and tool_name == "mcp__codingbuddy__parse_mode":
379+
return "done"
380+
381+
return None
382+
383+
384+
def _detect_blocker_count(
385+
tool_name: str,
386+
tool_input: dict,
387+
tool_output: str,
388+
) -> Optional[int]:
389+
"""Detect blocker count from quality-check Bash output.
390+
391+
Returns:
392+
int >= 0 when the tool is a quality-check command
393+
(0 means all checks passed — clear blockers).
394+
None for non-quality tools (meaning "don't touch blockerCount").
395+
"""
396+
if tool_name != "Bash":
397+
return None
398+
399+
cmd = tool_input.get("command", "")
400+
output = (tool_output or "").lower()
401+
402+
# Test runner
403+
if any(p in cmd for p in _TEST_CMD_PATTERNS):
404+
match = re.search(r"(\d+)\s+failed", output)
405+
if match:
406+
return int(match.group(1))
407+
if "failed" in output or "fail" in output:
408+
return 1
409+
return 0
410+
411+
# Type checker
412+
if any(p in cmd for p in _TYPECHECK_CMD_PATTERNS):
413+
errors = re.findall(r"error ts\d+", output)
414+
if errors:
415+
return len(errors)
416+
if "error" in output:
417+
return 1
418+
return 0
419+
420+
# Linter
421+
if any(p in cmd for p in _LINT_CMD_PATTERNS):
422+
if "error" in output:
423+
return 1
424+
return 0
425+
426+
return None
427+
428+
310429
def _detect_focus(tool_name: str, tool_input: dict) -> Optional[str]:
311430
"""Infer a human-readable focus label from the current tool call."""
312431
if tool_name == "Edit" or tool_name == "Write":

0 commit comments

Comments
 (0)