Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"requiredKeys": ["status", "summary", "next_action"],
"schema": {
"status": "SUCCESS|FAILURE|AMBIGUOUS",
"summary": "brief description",
"next_action": "proceed|retry|escalate"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Run the `{{label}}` TEA workflow for story `{{story_id}}`.

{{skill_line}}{{workflow_line}}{{instructions_line}}{{checklist_line}}{{template_line}}Use the story context already prepared by story automator.

Return a concise structured result that matches the configured parse schema.

{{extra_instruction}}
6 changes: 5 additions & 1 deletion skills/bmad-story-automator/src/story_automator/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
cmd_stop_hook,
)
from .commands.orchestrator import cmd_orchestrator_helper
from .commands.state import cmd_build_state_doc, cmd_sprint_compare, cmd_state_metrics, cmd_validate_state
from .commands.state import cmd_build_run_policy, cmd_build_state_doc, cmd_detect_workflow_track, cmd_sprint_compare, cmd_state_metrics, cmd_validate_state
from .commands.tmux import cmd_codex_status_check, cmd_heartbeat_check, cmd_monitor_session, cmd_tmux_status_check, cmd_tmux_wrapper
from .commands.validate_story_creation import cmd_validate_story_creation
from .core.common import help_flag, print_json
Expand All @@ -39,6 +39,8 @@ def main(argv: list[str] | None = None) -> int:
"ensure-stop-hook": cmd_ensure_stop_hook,
"stop-hook": cmd_stop_hook,
"build-state-doc": cmd_build_state_doc,
"build-run-policy": cmd_build_run_policy,
"detect-workflow-track": cmd_detect_workflow_track,
"commit-story": cmd_commit_story,
"parse-epic": _cmd_parse_epic,
"parse-story": _cmd_parse_story,
Expand Down Expand Up @@ -75,6 +77,8 @@ def _usage(stream: object) -> None:
"ensure-stop-hook",
"stop-hook",
"build-state-doc",
"build-run-policy",
"detect-workflow-track",
"commit-story",
"parse-epic",
"parse-story",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def cmd_orchestrator_helper(args: list[str]) -> int:
"state-latest-incomplete": _state_latest_incomplete,
"state-summary": _state_summary,
"state-update": _state_update,
"state-progress": _state_progress,
"escalate": _escalate,
"commit-ready": _commit_ready,
"normalize-key": _normalize_key,
Expand Down Expand Up @@ -100,6 +101,7 @@ def _usage(code: int) -> int:
print(" state-latest-incomplete <folder>", file=target)
print(" state-summary <file>", file=target)
print(" state-update <file> --set k=v", file=target)
print(" state-progress <file> --story ID --set step=value", file=target)
print(" escalate <trigger> <context>", file=target)
print(" commit-ready <story_id>", file=target)
print(" normalize-key <input> [--to id|key|prefix|json]", file=target)
Expand All @@ -110,7 +112,7 @@ def _usage(code: int) -> int:
print(" get-epic-stories <epic> [--state-file path]", file=target)
print(" check-blocking <story_id>", file=target)
print(" agents-build --state-file path --complexity-file path --output path --config-json '{}'", file=target)
print(" agents-resolve (--state-file path | --agents-file path) --story ID --task create|dev|auto|review", file=target)
print(" agents-resolve (--state-file path | --agents-file path) --story ID --task STEP_NAME", file=target)
print(" retro-agent --state-file path", file=target)
return code

Expand Down Expand Up @@ -475,6 +477,125 @@ def _verify_step(args: list[str]) -> int:
return exit_code


def _normalize_progress_key(value: str) -> str:
key = str(value or "").strip().lower().replace("_", "-")
aliases = {
"create": "create-story",
"dev": "dev-story",
"auto": "automate",
"review": "code-review",
"test-automate": "test-automate",
"test-review": "test-review",
"git_commit": "git-commit",
"git-commit": "git-commit",
"status": "status",
"story": "story",
"create-story": "create-story",
"dev-story": "dev-story",
"automate": "automate",
"code-review": "code-review",
"atdd": "atdd",
"nfr": "nfr",
"trace": "trace",
}
return aliases.get(key, key)


def _parse_markdown_cells(line: str) -> list[str]:
parts = [part.strip() for part in line.split("|")]
return [part for part in parts[1:-1]]


def _render_markdown_row(cells: list[str]) -> str:
return "| " + " | ".join(cells) + " |"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] orchestrator-helper now owns markdown progress-table parsing and mutation. _state_progress finds the table by line.startswith("| Story "), splits cells with line.split("|"), finds the story row by markdown prefix, rewrites the row, and writes the state file. That duplicates state document schema ownership outside the state-doc layer. Please keep this CLI as a thin entry point and move progress-table parse/update/render behind the canonical state/state-document layer.



def _state_progress(args: list[str]) -> int:
if not args:
print_json({"ok": False, "error": "file_not_found"})
return 1
state_file = args[0]
try:
if not file_exists(state_file):
print_json({"ok": False, "error": "file_not_found"})
return 1
except OSError:
print_json({"ok": False, "error": "state_file_unreadable"})
return 1
story_id = ""
updates: dict[str, str] = {}
idx = 1
while idx < len(args):
if args[idx] == "--story" and idx + 1 < len(args):
story_id = args[idx + 1]
idx += 2
continue
if args[idx] == "--set" and idx + 1 < len(args):
raw_update = args[idx + 1]
if "=" not in raw_update:
print_json({"ok": False, "error": "invalid_set_argument", "argument": raw_update})
return 1
key, value = raw_update.split("=", 1)
updates[_normalize_progress_key(key)] = value

@augmentcode augmentcode Bot May 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

skills/bmad-story-automator/src/story_automator/commands/orchestrator.py:539 — _state_progress() writes value directly into a Markdown table cell; if it contains | or newlines it can corrupt the progress table and break future parsing/updates. Consider validating/sanitizing --set values to a safe single-line, no-pipe subset before updating the row.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

idx += 2
continue
Comment thread
coderabbitai[bot] marked this conversation as resolved.
idx += 1
if not story_id or not updates:
print_json({"ok": False, "error": "missing_story_or_updates"})
return 1

try:
lines = read_text(state_file).splitlines()
except OSError:
print_json({"ok": False, "error": "state_file_unreadable"})
return 1
header_idx = -1
story_idx = -1
headers: list[str] = []
story_cells: list[str] = []
for i, line in enumerate(lines):
if line.startswith("| Story "):
header_idx = i
headers = [_normalize_progress_key(cell) for cell in _parse_markdown_cells(line)]
continue
if header_idx >= 0 and line.startswith(f"| {story_id} |"):
story_idx = i
story_cells = _parse_markdown_cells(line)
break
if header_idx < 0 or not headers:
print_json({"ok": False, "error": "progress_table_not_found"})
return 1
if story_idx < 0 or not story_cells:
print_json({"ok": False, "error": "story_row_not_found"})
return 1
if len(story_cells) != len(headers):
print_json({"ok": False, "error": "progress_row_misaligned"})
return 1
Comment thread
dickymoore marked this conversation as resolved.

header_map = {name: pos for pos, name in enumerate(headers)}
applied: list[str] = []
for key, value in updates.items():
if key == "story":
print_json({"ok": False, "error": "story_column_immutable"})
return 1
pos = header_map.get(key)
if pos is None:
continue
story_cells[pos] = value
applied.append(key)
if not applied:
print_json({"ok": False, "error": "progress_columns_not_found"})
return 1
lines[story_idx] = _render_markdown_row(story_cells)
try:
Path(state_file).write_text("\n".join(lines) + "\n", encoding="utf-8")
except OSError:
print_json({"ok": False, "error": "state_file_unwritable"})
return 1
print_json({"ok": True, "story": story_id, "updated": applied})
return 0


def _parse_context_int(context: str, key: str) -> int:
match = re.search(rf"{re.escape(key)}=(\d+)", context)
return int(match.group(1)) if match else 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pathlib import Path

from story_automator.core.frontmatter import extract_frontmatter, find_frontmatter_value, parse_frontmatter
from story_automator.core.runtime_policy import PolicyError, load_policy_shape_for_state, story_task_sequence
from story_automator.core.runtime_layout import runtime_provider
from story_automator.core.sprint import sprint_status_epic
from story_automator.core.story_keys import normalize_story_key
Expand Down Expand Up @@ -116,11 +117,17 @@ def agents_build_action(args: list[str]) -> int:
config = parse_agent_config(options["config-json"])
complexity = json.loads(read_text(options["complexity-file"]))
state_fields = parse_frontmatter(read_text(options["state-file"]))
try:
policy = load_policy_shape_for_state(options["state-file"])
tasks_in_scope = story_task_sequence(policy)
except PolicyError as exc:
print_json({"ok": False, "error": "policy_invalid", "reason": str(exc)})
return 1
stories = []
for story in complexity.get("stories", []):
level = str(story.get("complexity", {}).get("level", "medium")).lower() or "medium"
tasks = {}
for task in ("create", "dev", "auto", "review"):
for task in tasks_in_scope:
primary, fallback, model = resolve_agent(config, level, task)
entry = {
"primary": primary,
Expand Down
Loading