Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2240827
Add TEA-capable policy and state support
dickymoore May 22, 2026
04813c5
Add runtime TEA policy selection
dickymoore May 22, 2026
a21e5f6
Preserve standard-mode state and preflight UX
dickymoore May 22, 2026
de7d69b
Add TEA workflow detection
dickymoore May 22, 2026
7656470
Fix TEA review flow regressions
dickymoore May 23, 2026
cc08957
Accept canonical TEA skill names
dickymoore May 23, 2026
bbad902
Add minimal TEA adapter fallback
dickymoore May 23, 2026
e3859e9
Tighten TEA fallback and NFR gating
dickymoore May 23, 2026
8026802
Isolate standard mode from TEA overrides
dickymoore May 23, 2026
4c8b7b4
Keep standard mode isolated from TEA detection
dickymoore May 24, 2026
909b572
conflict resolution and PR comment resolution
dickymoore May 25, 2026
7036650
Tighten TEA PR comment follow-ups
dickymoore May 25, 2026
0532fa3
Merge upstream main into TEA workflow branch
dickymoore May 25, 2026
162a450
fix: avoid asset resolution for agent task sequencing
dickymoore May 25, 2026
c1ff3e9
fix: tighten PR review follow-ups
dickymoore May 25, 2026
a0a5f62
fix: address latest PR review comments
dickymoore May 25, 2026
05f3ff0
fix: tighten explicit policy detection
dickymoore May 25, 2026
4118637
fix: harden PR review follow-ups
dickymoore May 26, 2026
e4fd975
test: stabilize unreadable override regression
dickymoore May 26, 2026
ecb5ec6
Refactor TEA policy and state document handling
dickymoore May 26, 2026
8fd1857
Fix explicit policy override regressions
dickymoore May 26, 2026
cd12d4c
Fix TEA detection override precedence
dickymoore May 26, 2026
7874e8c
Fix PR review follow-ups
dickymoore May 27, 2026
c2a6664
Validate storyRange item types
dickymoore May 28, 2026
86e0596
Merge branch 'main' into refactor/tea-policy-core
dickymoore May 28, 2026
e320c26
Address maintainer TEA workflow follow-ups
dickymoore Jun 2, 2026
438cb97
Tighten TEA workflow policy guards
dickymoore Jun 3, 2026
7e45796
Merge branch 'main' into refactor/tea-policy-core
dickymoore Jun 3, 2026
4be658a
Fix June maintainer workflow regressions
dickymoore Jun 4, 2026
521f726
Add bounded maintainer review gate checks
dickymoore Jun 4, 2026
affe950
Move review gate note out of automator repo
dickymoore Jun 4, 2026
089740f
Make review gate smoke wrappers standalone
dickymoore Jun 4, 2026
5e9af12
Harden standalone smoke wrapper timeouts
dickymoore Jun 4, 2026
8e4f1b2
Merge branch 'main' into refactor/tea-policy-core
dickymoore Jun 5, 2026
51afb46
refactor: consolidate TEA policy boundaries
bma-d Jun 12, 2026
11fdd6f
fix: preserve state progress dependency patching
bma-d Jun 12, 2026
90fed7c
fix: address bot review feedback
bma-d Jun 12, 2026
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
58 changes: 58 additions & 0 deletions scripts/run-smoke-policy-invariants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
from __future__ import annotations

import json
import os
import subprocess
import sys
from pathlib import Path


MODULES = [
"tests.test_policy_invariants",
"tests.test_progress_invariants",
]


def _to_text(value: object) -> str:
if isinstance(value, bytes):
return value.decode("utf-8", errors="replace")
if isinstance(value, str):
return value
return ""


def main() -> int:
repo_root = Path(__file__).resolve().parents[1]
env = dict(os.environ)
pythonpath = str(repo_root / "skills" / "bmad-story-automator" / "src")
existing = env.get("PYTHONPATH", "").strip()
env["PYTHONPATH"] = pythonpath if not existing else f"{pythonpath}{os.pathsep}{existing}"
cmd = [sys.executable, "-m", "unittest", *MODULES]
try:
completed = subprocess.run(cmd, text=True, capture_output=True, cwd=repo_root, env=env, timeout=600)
except subprocess.TimeoutExpired as exc:
stdout = _to_text(exc.stdout)
stderr = _to_text(exc.stderr)
payload = {
"ok": False,
"modules": MODULES,
"returncode": 124,
"stdout": stdout,
"stderr": (stderr + "\nTimed out after 600 seconds").strip(),
}
print(json.dumps(payload))
return 124
payload = {
"ok": completed.returncode == 0,
"modules": MODULES,
"returncode": completed.returncode,
"stdout": completed.stdout,
"stderr": completed.stderr,
}
print(json.dumps(payload))
return completed.returncode


if __name__ == "__main__":
raise SystemExit(main())
57 changes: 57 additions & 0 deletions scripts/run-smoke-resume-matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
from __future__ import annotations

import json
import os
import subprocess
import sys
from pathlib import Path


MODULES = [
"tests.test_resume_matrix",
]


def _to_text(value: object) -> str:
if isinstance(value, bytes):
return value.decode("utf-8", errors="replace")
if isinstance(value, str):
return value
return ""


def main() -> int:
repo_root = Path(__file__).resolve().parents[1]
env = dict(os.environ)
pythonpath = str(repo_root / "skills" / "bmad-story-automator" / "src")
existing = env.get("PYTHONPATH", "").strip()
env["PYTHONPATH"] = pythonpath if not existing else f"{pythonpath}{os.pathsep}{existing}"
cmd = [sys.executable, "-m", "unittest", *MODULES]
try:
completed = subprocess.run(cmd, text=True, capture_output=True, cwd=repo_root, env=env, timeout=600)
except subprocess.TimeoutExpired as exc:
stdout = _to_text(exc.stdout)
stderr = _to_text(exc.stderr)
payload = {
"ok": False,
"modules": MODULES,
"returncode": 124,
"stdout": stdout,
"stderr": (stderr + "\nTimed out after 600 seconds").strip(),
}
print(json.dumps(payload))
return 124
payload = {
"ok": completed.returncode == 0,
"modules": MODULES,
"returncode": completed.returncode,
"stdout": completed.stdout,
"stderr": completed.stderr,
}
print(json.dumps(payload))
return completed.returncode


if __name__ == "__main__":
raise SystemExit(main())
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
}
},
"auto": {
"label": "qa-generate-e2e-tests",
"label": "automate",
"assets": {
"skillName": "bmad-qa-generate-e2e-tests",
"workflowCandidates": ["workflow.md", "workflow.yaml"],
Expand Down
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"
}
}
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 @@ -18,7 +18,6 @@
crash_max_retries,
load_runtime_policy,
review_max_cycles,
summarize_state_policy_fields,
)
from story_automator.core.review_verify import verify_code_review_completion
from story_automator.core.runtime_layout import active_marker_path, active_marker_project_entry
Expand Down Expand Up @@ -46,6 +45,16 @@
retro_agent_action,
)
from .orchestrator_parse import parse_output_action
from .orchestrator_state import (
policy_sequence_action,
policy_steps_action,
state_latest_action,
state_latest_incomplete_action,
state_list_action,
state_progress_action,
state_summary_action,
state_update_action,
)


def cmd_orchestrator_helper(args: list[str]) -> int:
Expand All @@ -58,11 +67,12 @@ def cmd_orchestrator_helper(args: list[str]) -> int:
"sprint-status": _sprint_status,
"parse-output": parse_output_action,
"marker": _marker,
"state-list": _state_list,
"state-latest": _state_latest,
"state-latest-incomplete": _state_latest_incomplete,
"state-summary": _state_summary,
"state-update": _state_update,
"state-list": state_list_action,
"state-latest": state_latest_action,
"state-latest-incomplete": state_latest_incomplete_action,
"state-summary": state_summary_action,
"state-update": state_update_action,
"state-progress": _state_progress_action,
"escalate": _escalate,
"commit-ready": _commit_ready,
"normalize-key": _normalize_key,
Expand All @@ -75,13 +85,19 @@ def cmd_orchestrator_helper(args: list[str]) -> int:
"agents-build": agents_build_action,
"agents-resolve": agents_resolve_action,
"retro-agent": retro_agent_action,
"policy-sequence": policy_sequence_action,
"policy-steps": policy_steps_action,
}
handler = dispatch.get(action)
if handler is None:
return _usage(1)
return handler(args[1:])


def _state_progress_action(args: list[str]) -> int:
return state_progress_action(args, exists_fn=file_exists)


def _usage(code: int) -> int:
target = __import__("sys").stderr if code else __import__("sys").stdout
print("Usage: orchestrator-helper <action> [args]", file=target)
Expand All @@ -101,6 +117,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 @@ -111,8 +128,10 @@ 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)
print(" policy-sequence [--state-file path]", file=target)
print(" policy-steps --group tea-quality [--state-file path]", file=target)
return code


Expand Down Expand Up @@ -220,108 +239,6 @@ def _marker(args: list[str]) -> int:
return 1


def _state_list(args: list[str]) -> int:
if not args or not Path(args[0]).is_dir():
print_json({"ok": False, "error": "folder_not_found", "files": []})
return 1
files = []
for path in sorted(Path(args[0]).glob("orchestration-*.md")):
files.append({"path": str(path), "status": find_frontmatter_value(path, "status") or "unknown", "lastUpdated": find_frontmatter_value(path, "lastUpdated") or "unknown"})
print_json({"ok": True, "files": files})
return 0


def _state_latest(args: list[str]) -> int:
if not args or not Path(args[0]).is_dir():
print_json({"ok": False, "error": "folder_not_found"})
return 1
status_filter = args[1] if len(args) > 1 else ""
matches = []
for path in Path(args[0]).glob("orchestration-*.md"):
status = find_frontmatter_value(path, "status")
if status_filter and status != status_filter:
continue
matches.append((find_frontmatter_value(path, "lastUpdated"), str(path)))
if not matches:
print_json({"ok": False, "error": "no_match"})
return 0
updated, path = max(matches)
print_json({"ok": True, "path": path, "lastUpdated": updated})
return 0


def _state_latest_incomplete(args: list[str]) -> int:
if not args or not Path(args[0]).is_dir():
print_json({"ok": False, "error": "folder_not_found"})
return 1
matches = []
for path in Path(args[0]).glob("orchestration-*.md"):
status = find_frontmatter_value(path, "status")
if status == "COMPLETE":
continue
matches.append((find_frontmatter_value(path, "lastUpdated"), status, str(path)))
if not matches:
print_json({"ok": False, "error": "no_incomplete_state"})
return 0
updated, status, path = max(matches)
print_json({"ok": True, "path": path, "lastUpdated": updated, "status": status})
return 0


def _state_summary(args: list[str]) -> int:
if not args or not file_exists(args[0]):
print_json({"ok": False, "error": "file_not_found"})
return 1
fields = parse_simple_frontmatter(read_text(args[0]))
snapshot_file, snapshot_hash, policy_version, legacy_policy, policy_error = summarize_state_policy_fields(
fields,
project_root=get_project_root(),
)
payload = {
"ok": True,
"epic": str(fields.get("epic") or ""),
"epicName": str(fields.get("epicName") or ""),
"currentStory": str(fields.get("currentStory") or ""),
"currentStep": str(fields.get("currentStep") or ""),
"status": str(fields.get("status") or ""),
"lastUpdated": str(fields.get("lastUpdated") or ""),
"policyVersion": policy_version,
"policySnapshotFile": snapshot_file,
"policySnapshotHash": snapshot_hash,
"legacyPolicy": legacy_policy,
"lastAction": extract_last_action(args[0]),
}
if policy_error:
payload["policyError"] = policy_error
print_json(payload)
return 0


def _state_update(args: list[str]) -> int:
if not args or not file_exists(args[0]):
print_json({"ok": False, "error": "file_not_found"})
return 1
text = read_text(args[0])
updated: list[str] = []
idx = 1
while idx < len(args):
if args[idx] == "--set" and idx + 1 < len(args):
key, value = args[idx + 1].split("=", 1)
replaced, count = re.subn(rf"(?m)^{re.escape(key)}:.*$", lambda m, k=key, v=value: f"{k}: {v}", text)
if count:
text = replaced
updated.append(key)
idx += 2
continue
idx += 1
if not updated:
print_json({"ok": False, "error": "keys_not_found", "updated": []})
return 1
Path(args[0]).write_text(text, encoding="utf-8")
print_json({"ok": True, "updated": updated})
return 0


def _escalate(args: list[str]) -> int:
trigger = args[0] if args else ""
context = args[1] if len(args) > 1 else ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from story_automator.core.artifact_paths import implementation_artifacts_dir
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 StoryKey, normalize_story_key, normalize_story_key_for_epic
Expand Down Expand Up @@ -138,11 +139,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