Skip to content

Commit 4826f63

Browse files
committed
feat(plugin): extend HUD state schema for richer workflow snapshots (#1326)
Add phase, focus, executionStrategy, councilStatus, blockerCount, and lastHandoff fields to init_hud_state. Add fill_defaults kwarg to read_hud_state for backward-compatible reads of older state files.
1 parent 2aff4ac commit 4826f63

2 files changed

Lines changed: 176 additions & 6 deletions

File tree

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

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""HUD state management for CodingBuddy statusLine (#1087).
1+
"""HUD state management for CodingBuddy statusLine (#1087, #1326).
22
33
Manages ~/.codingbuddy/hud-state.json shared between hooks.
44
Uses fcntl.flock() for file-level locking on every IO operation.
@@ -8,6 +8,16 @@
88
from datetime import datetime, timezone
99
from typing import Any, Dict
1010

11+
# Default values for extended schema fields (#1326).
12+
_EXTENDED_DEFAULTS: Dict[str, Any] = {
13+
"phase": "ready",
14+
"focus": None,
15+
"executionStrategy": None,
16+
"councilStatus": None,
17+
"blockerCount": 0,
18+
"lastHandoff": None,
19+
}
20+
1121
try:
1222
import fcntl
1323
HAS_FCNTL = True
@@ -23,19 +33,34 @@
2333
)
2434

2535

26-
def read_hud_state(state_file: str = DEFAULT_STATE_FILE) -> Dict[str, Any]:
36+
def read_hud_state(
37+
state_file: str = DEFAULT_STATE_FILE,
38+
*,
39+
fill_defaults: bool = False,
40+
) -> Dict[str, Any]:
2741
"""Read HUD state from JSON file with shared lock.
2842
43+
Args:
44+
state_file: Path to the state JSON file.
45+
fill_defaults: When True, back-fill missing extended-schema keys
46+
with their defaults so callers always see the full schema.
47+
2948
Returns empty dict on any error (missing file, parse error).
3049
"""
3150
try:
3251
with open(state_file, "r", encoding="utf-8") as f:
3352
if HAS_FCNTL:
3453
fcntl.flock(f.fileno(), fcntl.LOCK_SH)
35-
return json.load(f)
54+
data: Dict[str, Any] = json.load(f)
3655
except (json.JSONDecodeError, OSError):
3756
return {}
3857

58+
if fill_defaults:
59+
for key, default in _EXTENDED_DEFAULTS.items():
60+
data.setdefault(key, default)
61+
62+
return data
63+
3964

4065
def init_hud_state(
4166
session_id: str,
@@ -47,12 +72,19 @@ def init_hud_state(
4772
Creates parent directory if needed. Overwrites existing state.
4873
"""
4974
now = datetime.now(timezone.utc).isoformat()
50-
data = {
75+
data: Dict[str, Any] = {
5176
"sessionStartTimestamp": now,
5277
"sessionId": session_id,
5378
"version": version,
5479
"currentMode": None,
5580
"activeAgent": None,
81+
# Extended schema (#1326)
82+
"phase": "ready",
83+
"focus": None,
84+
"executionStrategy": None,
85+
"councilStatus": None,
86+
"blockerCount": 0,
87+
"lastHandoff": None,
5688
"updatedAt": now,
5789
}
5890
_locked_write(state_file, data)

packages/claude-code-plugin/tests/test_hud_state.py

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Tests for HUD state management module (#1087)."""
1+
"""Tests for HUD state management module (#1087, #1326)."""
22
import json
33
import os
44
import sys
@@ -12,7 +12,7 @@
1212
if _lib_dir not in sys.path:
1313
sys.path.insert(0, _lib_dir)
1414

15-
from hud_state import init_hud_state, read_hud_state, update_hud_state
15+
from hud_state import _EXTENDED_DEFAULTS, init_hud_state, read_hud_state, update_hud_state
1616

1717

1818
class TestReadHudState:
@@ -86,6 +86,144 @@ def test_noop_when_file_missing(self, tmp_path):
8686
update_hud_state(state_file=path, currentMode="PLAN")
8787

8888

89+
class TestExtendedSchemaDefaults:
90+
"""Tests for extended HUD schema fields (#1326)."""
91+
92+
def test_init_includes_extended_fields(self, tmp_path):
93+
path = str(tmp_path / "hud-state.json")
94+
init_hud_state("s1", "5.3.0", state_file=path)
95+
data = read_hud_state(path)
96+
97+
assert data["phase"] == "ready"
98+
assert data["focus"] is None
99+
assert data["executionStrategy"] is None
100+
assert data["councilStatus"] is None
101+
assert data["blockerCount"] == 0
102+
assert data["lastHandoff"] is None
103+
104+
def test_update_extended_fields(self, tmp_path):
105+
path = str(tmp_path / "hud-state.json")
106+
init_hud_state("s1", "5.3.0", state_file=path)
107+
108+
update_hud_state(
109+
state_file=path,
110+
phase="planning",
111+
focus="auth-feature",
112+
executionStrategy="subagent",
113+
blockerCount=2,
114+
)
115+
data = read_hud_state(path)
116+
117+
assert data["phase"] == "planning"
118+
assert data["focus"] == "auth-feature"
119+
assert data["executionStrategy"] == "subagent"
120+
assert data["blockerCount"] == 2
121+
# Unchanged fields preserved
122+
assert data["councilStatus"] is None
123+
assert data["lastHandoff"] is None
124+
125+
def test_partial_update_preserves_other_extended_fields(self, tmp_path):
126+
path = str(tmp_path / "hud-state.json")
127+
init_hud_state("s1", "5.3.0", state_file=path)
128+
129+
update_hud_state(state_file=path, phase="acting", councilStatus="quorum")
130+
update_hud_state(state_file=path, blockerCount=1)
131+
132+
data = read_hud_state(path)
133+
assert data["phase"] == "acting"
134+
assert data["councilStatus"] == "quorum"
135+
assert data["blockerCount"] == 1
136+
137+
138+
class TestBackwardCompat:
139+
"""Backward compatibility with older state files missing extended keys (#1326)."""
140+
141+
def test_read_old_state_without_fill(self, tmp_path):
142+
"""Reading an old-format file without fill_defaults returns raw data."""
143+
path = str(tmp_path / "old.json")
144+
old_data = {
145+
"sessionId": "old-1",
146+
"version": "5.1.0",
147+
"currentMode": "PLAN",
148+
"activeAgent": None,
149+
"updatedAt": "2026-01-01T00:00:00+00:00",
150+
"sessionStartTimestamp": "2026-01-01T00:00:00+00:00",
151+
}
152+
with open(path, "w") as f:
153+
json.dump(old_data, f)
154+
155+
result = read_hud_state(path)
156+
assert "phase" not in result
157+
assert "blockerCount" not in result
158+
159+
def test_read_old_state_with_fill_defaults(self, tmp_path):
160+
"""fill_defaults=True back-fills missing extended keys."""
161+
path = str(tmp_path / "old.json")
162+
old_data = {
163+
"sessionId": "old-1",
164+
"version": "5.1.0",
165+
"currentMode": "PLAN",
166+
"activeAgent": None,
167+
"updatedAt": "2026-01-01T00:00:00+00:00",
168+
"sessionStartTimestamp": "2026-01-01T00:00:00+00:00",
169+
}
170+
with open(path, "w") as f:
171+
json.dump(old_data, f)
172+
173+
result = read_hud_state(path, fill_defaults=True)
174+
assert result["phase"] == "ready"
175+
assert result["blockerCount"] == 0
176+
assert result["focus"] is None
177+
assert result["executionStrategy"] is None
178+
assert result["councilStatus"] is None
179+
assert result["lastHandoff"] is None
180+
# Original fields untouched
181+
assert result["sessionId"] == "old-1"
182+
assert result["currentMode"] == "PLAN"
183+
184+
def test_fill_defaults_does_not_overwrite_existing(self, tmp_path):
185+
"""fill_defaults must not overwrite keys already present."""
186+
path = str(tmp_path / "partial.json")
187+
partial = {
188+
"sessionId": "p-1",
189+
"phase": "acting",
190+
"blockerCount": 3,
191+
}
192+
with open(path, "w") as f:
193+
json.dump(partial, f)
194+
195+
result = read_hud_state(path, fill_defaults=True)
196+
assert result["phase"] == "acting"
197+
assert result["blockerCount"] == 3
198+
assert result["focus"] is None # filled
199+
200+
def test_fill_defaults_on_empty_returns_empty(self, tmp_path):
201+
"""fill_defaults on missing file still returns empty dict."""
202+
path = str(tmp_path / "nope.json")
203+
result = read_hud_state(path, fill_defaults=True)
204+
assert result == {}
205+
206+
def test_update_old_state_adds_new_field(self, tmp_path):
207+
"""Updating an old-format state file can add new fields."""
208+
path = str(tmp_path / "old.json")
209+
old_data = {
210+
"sessionId": "old-1",
211+
"version": "5.1.0",
212+
"currentMode": None,
213+
"activeAgent": None,
214+
"updatedAt": "2026-01-01T00:00:00+00:00",
215+
"sessionStartTimestamp": "2026-01-01T00:00:00+00:00",
216+
}
217+
with open(path, "w") as f:
218+
json.dump(old_data, f)
219+
220+
update_hud_state(state_file=path, phase="evaluating", blockerCount=1)
221+
result = read_hud_state(path)
222+
assert result["phase"] == "evaluating"
223+
assert result["blockerCount"] == 1
224+
assert result["sessionId"] == "old-1"
225+
226+
89227
class TestRoundtrip:
90228
def test_init_read_update_read(self, tmp_path):
91229
path = str(tmp_path / "hud-state.json")

0 commit comments

Comments
 (0)