Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Commit 083cc11

Browse files
committed
fix: address coderabbit diagnostics findings
1 parent cb3f5dd commit 083cc11

11 files changed

Lines changed: 77 additions & 20 deletions

File tree

skills/bmad-story-automator/data/tmux-commands.md

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,28 +38,21 @@ project_hash=$("$script" tmux-wrapper project-hash)
3838
**Generate full session name:**
3939
```bash
4040
script="$(printf "%s" "{project_root}/{installed-skill-root}/bmad-story-automator/scripts/story-automator")"
41-
project_slug=$("$script" tmux-wrapper project-slug)
42-
project_hash=$("$script" tmux-wrapper project-hash)
43-
timestamp=$(date +%y%m%d-%H%M%S) # Returns "260114-223045"
44-
session_name="sa-${project_slug}-${project_hash}-${timestamp}-e{epic}-s{story_suffix}-{step}"
41+
session_name=$("$script" tmux-wrapper name "{step}" "{epic}" "{story_id}")
4542
```
4643

4744
### Listing/Killing Project-Specific Sessions
4845

4946
**List only current project's sessions:**
5047
```bash
5148
script="$(printf "%s" "{project_root}/{installed-skill-root}/bmad-story-automator/scripts/story-automator")"
52-
project_slug=$("$script" tmux-wrapper project-slug)
53-
project_hash=$("$script" tmux-wrapper project-hash)
54-
tmux list-sessions 2>/dev/null | grep "^sa-${project_slug}-${project_hash}-"
49+
"$script" tmux-wrapper list --project-only
5550
```
5651

5752
**Kill only current project's sessions:**
5853
```bash
5954
script="$(printf "%s" "{project_root}/{installed-skill-root}/bmad-story-automator/scripts/story-automator")"
60-
project_slug=$("$script" tmux-wrapper project-slug)
61-
project_hash=$("$script" tmux-wrapper project-hash)
62-
tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^sa-${project_slug}-${project_hash}-" | xargs -I {} tmux kill-session -t {}
55+
"$script" tmux-wrapper kill-all --project-only
6356
```
6457

6558
### No Dots in Session Names

skills/bmad-story-automator/src/story_automator/commands/agent_config_cmd.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,6 @@ def _load_presets_or_report(file_path: str) -> dict | None:
9999
except json.JSONDecodeError:
100100
print_json({"ok": False, "error": "invalid_presets_json"})
101101
return None
102-
except OSError as exc:
102+
except (OSError, UnicodeDecodeError) as exc:
103103
print_json({"ok": False, "error": "presets_file_error", "reason": str(exc)})
104104
return None

skills/bmad-story-automator/src/story_automator/commands/orchestrator.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -301,15 +301,16 @@ def _state_update(args: list[str]) -> int:
301301
pending_status = value
302302
final_status = value
303303
updated: list[str] = []
304+
frontmatter, body = _split_frontmatter(text)
304305
for key, value in updates:
305-
replaced, count = re.subn(rf"(?m)^{re.escape(key)}:.*$", lambda m, k=key, v=value: f"{k}: {v}", text)
306+
replaced, count = re.subn(rf"(?m)^{re.escape(key)}:.*$", lambda m, k=key, v=value: f"{k}: {v}", frontmatter)
306307
if count:
307-
text = replaced
308+
frontmatter = replaced
308309
updated.append(key)
309310
if not updated:
310311
print_json({"ok": False, "error": "keys_not_found", "updated": []})
311312
return 1
312-
Path(args[0]).write_text(text, encoding="utf-8")
313+
Path(args[0]).write_text(frontmatter + body, encoding="utf-8")
313314
if final_status:
314315
emit_state_transition(args[0], result="applied", new_status=final_status)
315316
event_fields = [key for key in updated if key in {"epic", "currentStory", "currentStep", "lastUpdated"}]
@@ -319,6 +320,15 @@ def _state_update(args: list[str]) -> int:
319320
return 0
320321

321322

323+
def _split_frontmatter(text: str) -> tuple[str, str]:
324+
if not text.startswith("---"):
325+
return text, ""
326+
parts = text.split("---", 2)
327+
if len(parts) < 3:
328+
return text, ""
329+
return f"{parts[0]}---{parts[1]}---", parts[2]
330+
331+
322332
def _escalate(args: list[str]) -> int:
323333
trigger = args[0] if args else ""
324334
context = args[1] if len(args) > 1 else ""

skills/bmad-story-automator/src/story_automator/core/agent_config_frontmatter.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77

88
def extract_agent_config_frontmatter(frontmatter: str) -> dict[str, object]:
99
for index, raw_line in enumerate(frontmatter.splitlines()):
10-
stripped = raw_line.strip()
11-
if stripped.startswith("agentConfig:"):
10+
if raw_line.startswith("agentConfig:"):
1211
return _extract_agent_config_block(frontmatter.splitlines(), index)
12+
if raw_line.strip().startswith("agentConfig:"):
13+
raise ValueError("agentConfig must be a top-level frontmatter key")
1314
return {}
1415

1516

@@ -39,6 +40,8 @@ def _parse_indented_map(lines: list[str]) -> dict[str, object]:
3940
line = _strip_inline_yaml_comment(raw_line.rstrip())
4041
if not line.strip():
4142
continue
43+
if "\t" in line:
44+
raise ValueError("agentConfig block must use spaces, not tabs")
4245
indent = len(line) - len(line.lstrip(" "))
4346
if indent % 2 != 0:
4447
raise ValueError("agentConfig indentation must use two-space levels")

skills/bmad-story-automator/src/story_automator/core/diagnostics.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
r"(?i)\b(authorization|credential|password|secret|token|api[_-]?key|access[_-]?key)\b\s*[:=]\s*(?:(?:bearer|basic|token)\s+)?[^\s,;]+"
2020
)
2121
ABSOLUTE_PATH_WITH_EXT_RE = re.compile(
22-
r"(?<![\w.-])/(?:[^,\n;:]+/)+[^,\n;:]*?\.[A-Za-z0-9][A-Za-z0-9._-]*(?=$|[\s,;:)\]}\"'])"
22+
r"(?<![\w.-])(?:/(?:[^,\n;:]+/)+[^,\n;:]*?|[A-Za-z]:[\\/](?:[^,\n;:]+[\\/])+[^,\n;:]*?)\.[A-Za-z0-9][A-Za-z0-9._-]*(?=$|[\s,;:)\]}\"'])"
2323
)
24-
ABSOLUTE_PATH_RE = re.compile(r"(?<![\w.-])(?:/[^\s,;:]+)+")
24+
ABSOLUTE_PATH_RE = re.compile(r"(?<![\w.-])(?:/[^\s,;:]+|[A-Za-z]:[\\/][^\s,;:]+)+")
2525

2626

2727
@dataclass(frozen=True)
@@ -164,5 +164,5 @@ def _redact_string(value: str) -> str:
164164

165165
def _path_placeholder(match: re.Match[str]) -> str:
166166
path = match.group(0)
167-
name = Path(path).name
167+
name = path.replace("\\", "/").rstrip("/").rsplit("/", 1)[-1]
168168
return f"<path:{name}>" if name else "<path>"

skills/bmad-story-automator/src/story_automator/core/state_validation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def parse_state_update_argument(raw: str) -> tuple[str, str] | dict[str, Any]:
145145
key, value = raw.split("=", 1)
146146
if not key.strip():
147147
return state_update_argument_error_payload(raw)
148-
return key, value
148+
return key.strip(), value
149149

150150

151151
def state_validation_payload(issues: list[DiagnosticIssue]) -> dict[str, Any]:

tests/test_cli_contracts.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,14 @@ def test_malformed_presets_file_returns_stable_error(self) -> None:
162162
self.assertEqual(code, 1)
163163
self.assertEqual(payload["error"], "invalid_presets_json")
164164

165+
def test_presets_decode_error_returns_stable_error(self) -> None:
166+
self.presets.write_bytes(b"\xff")
167+
168+
code, payload = self._agent(["list", "--file", str(self.presets)])
169+
170+
self.assertEqual(code, 1)
171+
self.assertEqual(payload["error"], "presets_file_error")
172+
165173
def _agent(self, args: list[str]) -> tuple[int, dict[str, object]]:
166174
stdout = io.StringIO()
167175
with redirect_stdout(stdout):

tests/test_diagnostics.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ def test_redact_actual_masks_absolute_paths_with_spaces(self) -> None:
131131
self.assertNotIn("My Project", redacted)
132132
self.assertNotIn("private/state.md", redacted)
133133

134+
def test_redact_actual_masks_windows_absolute_paths(self) -> None:
135+
redacted = redact_actual(r"C:\Users\joon\private\state.md token=abc123")
136+
137+
self.assertEqual(redacted, "<path:state.md> token=<redacted>")
138+
self.assertNotIn(r"C:\Users", redacted)
139+
self.assertNotIn(r"private\state.md", redacted)
140+
134141
def test_redact_actual_limits_nested_collections(self) -> None:
135142
payload = redact_actual({"values": list(range(10)), **{f"k{i}": i for i in range(10)}})
136143

tests/test_retro_agent.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,9 @@ def test_retro_agent_rejects_invalid_nested_complexity_override_frontmatter(self
330330
"---\nagentConfig:\n complexityOverrides:\n medium:\n retro:\n primary: \"codex\"\n---\n",
331331
"---\nagentConfig:\n defaultPrimary: \"claude\"\n complexityOverrides:\n medium:\n retro:\n primary: \"codex\"\n---\n",
332332
"---\nagentConfig: bad\n complexityOverrides:\n medium:\n retro:\n primary: \"codex\"\n---\n",
333+
"---\n agentConfig: {defaultPrimary: codex}\n---\n",
333334
"---\nagentConfig:\n\tdefaultPrimary: \"claude\"\n\tcomplexityOverrides:\n\t medium:\n\t retro:\n\t primary: \"codex\"\n---\n",
335+
"---\nagentConfig:\n \tdefaultPrimary: \"claude\"\n---\n",
334336
"---\nagentConfig:\ncomplexityOverrides:\n medium:\n retro:\n primary: \"codex\"\n---\n",
335337
)
336338
for index, content in enumerate(cases):

tests/test_state_validation.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def test_runtime_command_config_rejects_whitespace_only_command(self) -> None:
6060
self.assertFalse(has_runtime_command_config({"aiCommand": ["", " "]}, ""))
6161
self.assertTrue(has_runtime_command_config({"aiCommand": [" claude "]}, ""))
6262
self.assertTrue(has_runtime_command_config({"aiCommand": " "}, 'agentConfig:\n defaultPrimary: "codex"\n'))
63+
self.assertFalse(has_runtime_command_config({"aiCommand": " "}, ' agentConfig:\n defaultPrimary: "codex"\n'))
6364
self.assertFalse(has_runtime_command_config({"aiCommand": " "}, "agentConfig:\n defaultPrimary:\n"))
6465
self.assertFalse(has_runtime_command_config({"aiCommand": " "}, "agentConfig:\n complexityOverrides:\n - medium:\n"))
6566

@@ -247,6 +248,31 @@ def test_state_update_still_allows_non_status_updates(self) -> None:
247248
self.assertEqual(payload, {"ok": True, "updated": ["aiCommand"]})
248249
self.assertIn("aiCommand: claude --resume", state_file.read_text(encoding="utf-8"))
249250

251+
def test_state_update_only_rewrites_frontmatter(self) -> None:
252+
state_file = self._build_state_config(status="COMPLETE")
253+
text = state_file.read_text(encoding="utf-8").replace("currentStep: null\n", "currentStep: step-old\n", 1)
254+
state_file.write_text(text + "\nstatus: body-marker\ncurrentStep: body-step\n", encoding="utf-8")
255+
256+
code, payload = self._state_update_args(state_file, ["--set", "status=COMPLETE", "--set", "currentStep=step-next"])
257+
258+
self.assertEqual(code, 0)
259+
self.assertEqual(payload, {"ok": True, "updated": ["status", "currentStep"]})
260+
text = state_file.read_text(encoding="utf-8")
261+
frontmatter = text.split("---", 2)[1]
262+
body = text.split("---", 2)[2]
263+
self.assertIn("status: COMPLETE", frontmatter)
264+
self.assertIn("currentStep: step-next", frontmatter)
265+
self.assertIn("status: body-marker", body)
266+
self.assertIn("currentStep: body-step", body)
267+
268+
def test_state_update_strips_set_key_whitespace(self) -> None:
269+
state_file = self._build_state_config(status="READY")
270+
271+
code, payload = self._state_update(state_file, " status=COMPLETE")
272+
273+
self.assertEqual(code, 1)
274+
self.assertEqual(payload["error"], "invalid_status_transition")
275+
250276
def _validate_state(self, state_file: Path) -> dict[str, object]:
251277
stdout = io.StringIO()
252278
with patch_env(self.project_root), redirect_stdout(stdout):

0 commit comments

Comments
 (0)