Skip to content

feat: add Codex runtime support#3

Open
dickymoore wants to merge 3 commits intobmad-code-org:mainfrom
dickymoore:dicky/codex-runtime-support
Open

feat: add Codex runtime support#3
dickymoore wants to merge 3 commits intobmad-code-org:mainfrom
dickymoore:dicky/codex-runtime-support

Conversation

@dickymoore
Copy link
Copy Markdown

@dickymoore dickymoore commented May 1, 2026

Summary

Adds Codex runtime support alongside Claude Code in BMad Automator, rebased onto the upstream self-contained skills/ plugin layout.

This PR now folds in the correctness, compatibility, platform-testability, and operator-facing doc fixes needed to make the runtime change set robust instead of leaving those concerns split across too many follow-ups.

Main fixes in this PR

  • separate top-level runtime/provider detection from child-agent selection
  • stop using AI_AGENT to decide hook/config layout and marker defaults
  • preserve legacy fallback behavior for missing/unversioned agent config instead of silently disabling fallback
  • make Codex stop-hook results distinguish configured-on-disk vs trusted-and-verified state
  • pass PROJECT_ROOT through installed stop-hook commands so marker lookup does not depend on the caller's working directory
  • make legacy AI_COMMAND classification consistent with build/monitor behavior
  • document the actual runtime/provider resolution model and its precedence
  • make CLI/troubleshooting examples copy-paste-safe by resolving the installed helper from supported skill roots
  • normalize runtime-layout path assertions so the targeted Python tests stay valid on macOS path layouts as well as Linux
  • keep state/validation guidance aligned with the aiCommand / agentConfig runtime model already supported by the code

Scope Notes

This PR is still intentionally bounded to Codex runtime support plus the fixes needed to make that behavior safe, testable, and reviewable.

What remains intentionally out of scope:

  • cleanup-only stop-hook internals
  • cosmetic formatting-only fixes
  • any broader architectural redesign of .agents neutrality beyond documenting the current runtime model accurately

Tests

  • PYTHONPATH=skills/bmad-story-automator/src python3 -m pytest tests/test_runtime_layout.py tests/test_stop_hooks.py tests/test_state_policy_metadata.py tests/test_success_verifiers.py -q
  • current result on the pushed branch: 126 passed

Companion Work

Remaining Follow-ups

  1. Cleanup-only stop-hook/internal refactors after runtime behavior lands.
  2. Any optional broader redesign of shared .agents runtime semantics, if we decide to go beyond documenting the current behavior.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR generalizes runtime and installation paths beyond Claude to support multiple providers (Claude/Codex/.agents), makes retrospectives use the configured retro agent by default, implements provider-aware stop-hook installers (Claude JSON and Codex TOML), adds runtime-layout and marker resolution utilities, extends orchestrator helper actions, updates workflow steps and docs, and adds tests for layout, stop-hooks, and retro-agent resolution.

Changes

Runtime-aware retrospectives, marker handling, and stop-hook support

Layer / File(s) Summary
Data / Shape
source/src/story_automator/core/agent_config.py, source/src/story_automator/commands/state.py
Agent-config parsing now accepts a top-level retro entry, normalizes per-task maps, and defaults defaultPrimary to "auto" and defaultFallback to "false"; state doc generation preserves legacy top-level agentConfig.retro into perTask.retro when present.
Core Implementation
source/src/story_automator/core/runtime_layout.py, source/src/story_automator/core/runtime_policy.py
New runtime-layout utilities for provider detection, candidate skills-root discovery, portable path resolution, and active-marker resolution; runtime_policy delegates path/asset resolution to layout helpers and adds workflow/portable-path rules and SKILL.md acceptance for required workflows.
Provider Logic
source/src/story_automator/core/stop_hooks.py
New stop-hook subsystem with ensure_stop_hook dispatcher, Claude JSON hook handling, Codex hooks + TOML feature-toggle logic, atomic-write helpers, structured HookConfigError, and normalization/idempotency behaviors.
CLI / Wiring
source/src/story_automator/commands/basic.py, source/src/story_automator/commands/orchestrator.py, source/src/story_automator/commands/orchestrator_epic_agents.py, source/src/story_automator/commands/tmux.py
cmd_ensure_stop_hook now derives provider via runtime_provider() and returns structured JSON on errors; cmd_stop_hook and orchestrator helper use active_marker_path(); orchestrator helper adds marker path and retro-agent actions; new retro_agent_action reads state/frontmatter and resolves retro agent; tmux commands resolve "auto" via runtime_provider.
Workflow / Steps
payload/.../steps-c/step-01-init.md, step-01b-continue.md, step-02b-preflight-finalize.md, step-03b-execute-finish.md, step-04-wrapup.md
Removed hardcoded markerFile frontmatter; marker create/remove and gitignore handling now use orchestrator-helper marker path/create/remove and ensure-marker-gitignore; retrospectives spawn using the helper-resolved retro agent.
Installer & Scripts
install.sh, scripts/smoke-test.sh
Installer and smoke test generalized to install/verify under multiple supported skill roots (.agents/skills, .claude/skills, .codex/skills), loop per-root, and parameterize test helpers; installer selects complete roots and reports diagnostics when none are complete.
Documentation & Payload
README.md, docs/*.md, payload/.claude/skills/.../data/*, workflow.md
Docs and payload prompts/workflow references updated to use <installed-skill-root>/... paths, describe supported skill roots and runtime/provider env-vars, and state retrospectives inherit configured primary agent by default.
Tests
source/tests/test_runtime_layout.py, source/tests/test_stop_hooks.py, source/tests/test_state_policy_metadata.py
Added tests covering runtime-layout/provider/marker resolution, stop-hook install/idempotency/TOML formats and error paths, and retro-agent resolution/legacy compatibility/YAML-comment handling.

Sequence Diagram

sequenceDiagram
    participant User as User/Orchestrator
    participant Script as Workflow Script
    participant Helper as orchestrator-helper
    participant Layout as runtime_layout
    participant StopHooks as stop_hooks
    participant Provider as Claude/Codex

    User->>Script: start workflow
    Script->>Helper: orchestrator-helper marker path
    Helper->>Layout: active_marker_path()
    Layout-->>Helper: {file, entry}
    Helper-->>Script: JSON {file,entry}
    Script->>Script: ensure-marker-gitignore (entry)

    Script->>Helper: orchestrator-helper retro-agent --state-file
    Helper->>Helper: load agentConfig from state/frontmatter
    Helper->>Layout: runtime_provider()
    Helper-->>Script: {primary,fallback} for retro

    Script->>Script: tmux-wrapper build-cmd/spawn using resolved agent
    Script->>Helper: orchestrator-helper marker create
    Helper->>Layout: active_marker_path()
    Helper->>Provider: write marker at resolved location

    alt Install / Ensure Stop Hook
        Script->>Helper: orchestrator-helper ensure-stop-hook
        Helper->>StopHooks: ensure_stop_hook(provider=...)
        StopHooks->>Provider: update provider-specific config (JSON/TOML)
        StopHooks-->>Helper: {changed,reason,...}
    end

    Script->>Helper: orchestrator-helper marker remove
    Helper->>Layout: active_marker_path()
    Helper->>Provider: remove marker
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • bma-d

Poem

🐇 I nibble code and plant a root,
Retros leap where helpers boot.
Markers move to runtime ground,
Hooks install and calm the sound.
Hooray — the orchestrator's new route!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@dickymoore dickymoore force-pushed the dicky/codex-runtime-support branch from 7707edc to d07c0ff Compare May 1, 2026 17:35
@dickymoore dickymoore changed the title Add Codex runtime layout support Add Codex runtime support May 2, 2026
@dickymoore dickymoore marked this pull request as ready for review May 3, 2026 08:53
@dickymoore dickymoore requested a review from bma-d as a code owner May 3, 2026 08:53
@dickymoore dickymoore requested a review from bmadcode May 3, 2026 08:54
@augmentcode
Copy link
Copy Markdown

augmentcode Bot commented May 3, 2026

🤖 Augment PR Summary

Summary: This PR adds first-class Codex runtime layout support to Story Automator so installers/consumers no longer need to patch paths and hook behavior post-install.

Changes:

  • Introduced a deterministic runtime layout helper (runtime_layout.py) to infer provider, resolve active skill roots, and compute project-scoped marker paths.
  • Made marker/state-file resolution runtime-aware (no longer hard-coded to .claude), and added orchestrator-helper marker path to expose the resolved marker path + gitignore entry.
  • Refactored stop-hook installation into a provider-aware implementation (stop_hooks.py) supporting Claude (.claude/settings.json) and Codex (.codex/hooks.json + .codex/config.toml).
  • Added a retro-agent helper action and updated retrospective spawning to use the configured retro agent rather than forcing Claude.
  • Updated runtime policy asset/path resolution to support “portable” skill paths across .claude/.agents/.codex roots.
  • Extended docs and payload step instructions to use the new runtime-aware helper commands.
  • Added unit tests covering runtime layout detection, marker path behavior, Codex hook config mutation, and retro agent resolution.

Technical Notes: Provider selection can be forced via env vars (e.g., AI_AGENT), Codex hook enablement is ensured via TOML edits, and marker location is decoupled from provider to follow the active installed skill root where possible.

🤖 Was this summary useful? React with 👍 or 👎

Copy link
Copy Markdown

@augmentcode augmentcode Bot left a comment

Choose a reason for hiding this comment

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

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread source/src/story_automator/core/runtime_layout.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
source/src/story_automator/commands/orchestrator_epic_agents.py (1)

233-331: 💤 Low value

Consider documenting the expected YAML structure.

This custom indentation-based YAML parser handles the agentConfig frontmatter section with specific indent levels (2/4/6/8 spaces). The implementation appears correct for the expected structure, but the tight coupling to specific indentation could be fragile if the frontmatter format evolves.

The final parse_agent_config(json.dumps(config)) call ensures consistent post-processing, which is good.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@source/src/story_automator/commands/orchestrator_epic_agents.py` around lines
233 - 331, The _load_agent_config_from_state function uses a custom
indentation-based parser for the agentConfig frontmatter and is tightly coupled
to exact indent levels (2/4/6/8) and keys like "agentConfig:", "perTask:",
"complexityOverrides:", and "retro:"—document the expected frontmatter schema
and indentation rules immediately above the _load_agent_config_from_state
definition (or as its docstring) describing required top-level marker
"agentConfig", the allowed child sections ("perTask", "complexityOverrides",
"retro"), the precise indent levels for section headers (2 spaces) and nested
keys (4/6/8 spaces), and example snippets for each structure so future authors
know how to format input and what the parser expects; also mention that
parse_agent_config(json.dumps(config)) is applied to the constructed dict for
post-processing so callers can rely on that normalization.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@payload/.claude/skills/bmad-story-automator/data/stop-hook-troubleshooting.md`:
- Around line 113-114: The troubleshooting hint in stop-hook-troubleshooting.md
hardcodes the path ".claude/skills/bmad-story-automator/scripts/" which misleads
users on .agents/.codex runtimes; update the table entry to mention and check
all possible runtime skill trees (e.g., .claude, .agents, .codex) or provide a
single command that checks each location (for example suggest running ls -la
against .claude/skills/... OR .agents/skills/... OR .codex/skills/...) and/or
recommend using a glob like */skills/bmad-story-automator/scripts/ so the
diagnostic works for Codex and Agents layouts; change the exact hardcoded path
string in the table row to the more general variants so the troubleshooting flow
no longer assumes only the .claude layout.

In `@source/tests/test_stop_hooks.py`:
- Line 224: The S108 linter flag on the assertion accessing
config["projects"]["/tmp/example"] is a false positive because "/tmp/example" is
a TOML table key in the test fixture, so suppress the warning by adding a
per-line noqa: S108 comment to the assertion containing
config["projects"]["/tmp/example"] (the self.assertEqual(... trust_level ...)
line) so the linter ignores it without changing test semantics.
- Around line 139-147: The test asserts changed=False even though normalization
mutates hooks.json (timeout/statusMessage), which is confusing; update the
code/tests to clarify the semantic contract: either add a concise inline comment
in source/tests/test_stop_hooks.py next to the _run_ensure_stop_hook("codex")
assertion explaining that changed==True denotes a newly registered hook
requiring restart while normalization-only writes still return changed==False,
or update the production cmd_ensure_stop_hook function/method
docstring/return-schema to explicitly state that normalization writes do not set
changed=True; reference the symbols _run_ensure_stop_hook and
cmd_ensure_stop_hook (and the hooks.json normalization behavior) when adding the
comment or docstring so readers can locate the behavior.

---

Nitpick comments:
In `@source/src/story_automator/commands/orchestrator_epic_agents.py`:
- Around line 233-331: The _load_agent_config_from_state function uses a custom
indentation-based parser for the agentConfig frontmatter and is tightly coupled
to exact indent levels (2/4/6/8) and keys like "agentConfig:", "perTask:",
"complexityOverrides:", and "retro:"—document the expected frontmatter schema
and indentation rules immediately above the _load_agent_config_from_state
definition (or as its docstring) describing required top-level marker
"agentConfig", the allowed child sections ("perTask", "complexityOverrides",
"retro"), the precise indent levels for section headers (2 spaces) and nested
keys (4/6/8 spaces), and example snippets for each structure so future authors
know how to format input and what the parser expects; also mention that
parse_agent_config(json.dumps(config)) is applied to the constructed dict for
post-processing so callers can rely on that normalization.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0d781ed9-54d7-48a6-8796-b9dadd96fcf8

📥 Commits

Reviewing files that changed from the base of the PR and between 9e7dff8 and 0e8e613.

📒 Files selected for processing (30)
  • README.md
  • docs/agents-and-monitoring.md
  • docs/story-execution.md
  • payload/.claude/skills/bmad-story-automator/data/agent-config-prompts.md
  • payload/.claude/skills/bmad-story-automator/data/marker-file-format.md
  • payload/.claude/skills/bmad-story-automator/data/orchestrator-rules-appendix.md
  • payload/.claude/skills/bmad-story-automator/data/retrospective-automation.md
  • payload/.claude/skills/bmad-story-automator/data/scripts-reference.md
  • payload/.claude/skills/bmad-story-automator/data/stop-hook-config.md
  • payload/.claude/skills/bmad-story-automator/data/stop-hook-recovery.md
  • payload/.claude/skills/bmad-story-automator/data/stop-hook-troubleshooting.md
  • payload/.claude/skills/bmad-story-automator/data/tmux-long-command-testing.md
  • payload/.claude/skills/bmad-story-automator/steps-c/step-01-init.md
  • payload/.claude/skills/bmad-story-automator/steps-c/step-01b-continue.md
  • payload/.claude/skills/bmad-story-automator/steps-c/step-02a-preflight-config.md
  • payload/.claude/skills/bmad-story-automator/steps-c/step-02b-preflight-finalize.md
  • payload/.claude/skills/bmad-story-automator/steps-c/step-03b-execute-finish.md
  • payload/.claude/skills/bmad-story-automator/steps-c/step-04-wrapup.md
  • payload/.claude/skills/bmad-story-automator/workflow.md
  • source/src/story_automator/commands/basic.py
  • source/src/story_automator/commands/orchestrator.py
  • source/src/story_automator/commands/orchestrator_epic_agents.py
  • source/src/story_automator/commands/state.py
  • source/src/story_automator/core/agent_config.py
  • source/src/story_automator/core/runtime_layout.py
  • source/src/story_automator/core/runtime_policy.py
  • source/src/story_automator/core/stop_hooks.py
  • source/tests/test_runtime_layout.py
  • source/tests/test_state_policy_metadata.py
  • source/tests/test_stop_hooks.py
💤 Files with no reviewable changes (1)
  • payload/.claude/skills/bmad-story-automator/steps-c/step-02a-preflight-config.md

Comment thread payload/.claude/skills/bmad-story-automator/data/stop-hook-troubleshooting.md Outdated
Comment thread tests/test_stop_hooks.py
Comment thread tests/test_stop_hooks.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
docs/installation-and-layout.md (1)

22-27: ⚡ Quick win

Clarify Codex path split between skill root and hook/config root.

This doc correctly uses .agents/skills/... for installed skills, but it should explicitly note that Codex stop-hook/config files are under .codex/... to avoid operator confusion when wiring runtime behavior.

Proposed doc tweak
 The installer writes into the target project's native runtime skill root:

 - Claude Code: `.claude/skills/bmad-story-automator`
 - Claude Code: `.claude/skills/bmad-story-automator-review`
 - Codex: `.agents/skills/bmad-story-automator`
 - Codex: `.agents/skills/bmad-story-automator-review`
+
+Note: for Codex, skill assets live under `.agents/skills/...`, while hook/config files are managed under `.codex/...`.

Also applies to: 88-93

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/installation-and-layout.md` around lines 22 - 27, Update the
installation-and-layout.md section that lists install paths to explicitly
distinguish Codex runtime skill roots from Codex hook/config roots: keep the
installed skill paths as `.agents/skills/bmad-story-automator` and
`.agents/skills/bmad-story-automator-review` but add a clarifying sentence that
Codex stop-hook and configuration files live under `.codex/...` (e.g.,
`.codex/.../bmad-story-automator`), and mirror the same clarification for the
repeated mention at lines ~88-93 so readers understand where to wire runtime
behavior versus hook/config files.
source/src/story_automator/core/stop_hooks.py (1)

352-365: 💤 Low value

Optional: preserve a space before the trailing comment.

When rewriting an inline features = { ... } # comment, the regex consumes the whitespace between } and #, and the formatter then concatenates comment directly, producing }# comment. It's still valid TOML and the tests pass, but a single space would keep the file visually identical to common formatting.

♻️ Suggested cosmetic tweak
-    indent, inner, comment = match.group(1), match.group(2), match.group(3) or ""
+    indent, inner, comment = match.group(1), match.group(2), match.group(3) or ""
+    if comment:
+        comment = f" {comment}"
@@
-    return f"{indent}features = {{ {', '.join(updated_items)} }}{comment}"
+    return f"{indent}features = {{ {', '.join(updated_items)} }}{comment}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/src/story_automator/core/stop_hooks.py` around lines 352 - 365, The
function _set_inline_features_table_line currently drops the space between the
closing brace and a trailing comment because match.group(3) consumes the
intervening whitespace; update it so when a comment exists you prepend a single
space before appending it. Locate _set_inline_features_table_line, keep the
existing item parsing and replacement logic, and change how comment is
built/appended (e.g., if match.group(3) is truthy ensure comment starts with a
single space before the "#" token) so the returned string uses " ... }}<space>#
comment" instead of " ... }}# comment".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@docs/installation-and-layout.md`:
- Around line 22-27: Update the installation-and-layout.md section that lists
install paths to explicitly distinguish Codex runtime skill roots from Codex
hook/config roots: keep the installed skill paths as
`.agents/skills/bmad-story-automator` and
`.agents/skills/bmad-story-automator-review` but add a clarifying sentence that
Codex stop-hook and configuration files live under `.codex/...` (e.g.,
`.codex/.../bmad-story-automator`), and mirror the same clarification for the
repeated mention at lines ~88-93 so readers understand where to wire runtime
behavior versus hook/config files.

In `@source/src/story_automator/core/stop_hooks.py`:
- Around line 352-365: The function _set_inline_features_table_line currently
drops the space between the closing brace and a trailing comment because
match.group(3) consumes the intervening whitespace; update it so when a comment
exists you prepend a single space before appending it. Locate
_set_inline_features_table_line, keep the existing item parsing and replacement
logic, and change how comment is built/appended (e.g., if match.group(3) is
truthy ensure comment starts with a single space before the "#" token) so the
returned string uses " ... }}<space># comment" instead of " ... }}# comment".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cdbfc780-04f5-4981-a66b-25afd01ac783

📥 Commits

Reviewing files that changed from the base of the PR and between 0e8e613 and ccedfad.

📒 Files selected for processing (14)
  • README.md
  • docs/agents-and-monitoring.md
  • docs/cli-reference.md
  • docs/how-it-works.md
  • docs/installation-and-layout.md
  • docs/troubleshooting.md
  • payload/.claude/skills/bmad-story-automator/data/stop-hook-troubleshooting.md
  • source/src/story_automator/commands/basic.py
  • source/src/story_automator/commands/orchestrator_epic_agents.py
  • source/src/story_automator/core/runtime_layout.py
  • source/src/story_automator/core/stop_hooks.py
  • source/tests/test_runtime_layout.py
  • source/tests/test_state_policy_metadata.py
  • source/tests/test_stop_hooks.py
✅ Files skipped from review due to trivial changes (5)
  • docs/cli-reference.md
  • docs/troubleshooting.md
  • docs/how-it-works.md
  • README.md
  • docs/agents-and-monitoring.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • source/tests/test_runtime_layout.py
  • payload/.claude/skills/bmad-story-automator/data/stop-hook-troubleshooting.md
  • source/tests/test_state_policy_metadata.py
  • source/src/story_automator/core/runtime_layout.py
  • source/src/story_automator/commands/basic.py

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@source/src/story_automator/core/stop_hooks.py`:
- Around line 352-365: The function _set_inline_features_table_line is losing
the space between the closing brace and a trailing inline comment because
match.group(3) contains the comment token "#..." but not the separator space;
when rebuilding the line ensure you preserve that separator by prefixing a
single space when a trailing comment exists (e.g., compute comment =
match.group(3) or "" and then use comment = (" " + comment) if comment and not
comment.startswith(" ") else comment) before returning the formatted string so
features = {{ ... }} # comment keeps its original space; update only
_set_inline_features_table_line accordingly (no regex change required) and keep
returned format f"{indent}features = {{ {', '.join(updated_items)} }}{comment}".
- Around line 256-259: Remove the unused wrapper function
_ensure_codex_hooks_feature: delete its definition (the function that calls
_prepare_codex_hooks_feature, then _write_prepared_update, and returns
update.result) because ensure_codex_stop_hook already calls
_prepare_codex_hooks_feature and handles writes directly; ensure any imports or
references to _ensure_codex_hooks_feature are also removed so there are no
dangling references, and run tests to verify nothing else depends on
_ensure_codex_hooks_feature.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 585e2367-a0f2-4043-a187-6c01c31867a2

📥 Commits

Reviewing files that changed from the base of the PR and between ccedfad and 01f412d.

📒 Files selected for processing (8)
  • README.md
  • docs/agents-and-monitoring.md
  • docs/cli-reference.md
  • docs/how-it-works.md
  • docs/installation-and-layout.md
  • docs/troubleshooting.md
  • source/src/story_automator/core/stop_hooks.py
  • source/tests/test_stop_hooks.py
✅ Files skipped from review due to trivial changes (5)
  • docs/agents-and-monitoring.md
  • docs/cli-reference.md
  • README.md
  • docs/how-it-works.md
  • docs/installation-and-layout.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/troubleshooting.md

Comment thread skills/bmad-story-automator/src/story_automator/core/stop_hooks.py
Comment thread skills/bmad-story-automator/src/story_automator/core/stop_hooks.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
source/src/story_automator/commands/state.py (1)

240-251: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

validate-state still requires aiCommand, contradicting the new deprecated/empty default.

The state template now defaults aiCommand to "" (deprecated, replaced by agentConfig), and cmd_build_state_doc no longer enforces a non-empty value. However, cmd_validate_state still calls required("aiCommand"), which flags any empty string as "Missing or empty aiCommand". State documents generated under the new agentConfig-only flow will therefore fail validation even though they are correct.

Consider relaxing the check so aiCommand is optional when agentConfig is present (or drop the requirement entirely since the field is documented as deprecated).

🛡️ Possible adjustment
     required("status", lambda value: isinstance(value, str) and value in allowed)
     required("lastUpdated", lambda value: isinstance(value, str) and re.search(r"\d{4}-\d{2}-\d{2}T", value))
-    required("aiCommand")
+    # aiCommand is deprecated in favor of agentConfig; only required if agentConfig is absent.
+    if not fields.get("agentConfig"):
+        required("aiCommand")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/src/story_automator/commands/state.py` around lines 240 - 251, The
validator currently calls required("aiCommand") and flags empty strings even
though aiCommand is deprecated; update the validation in the state validation
function (the block that calls required(...) and load_policy_for_state) to make
aiCommand optional when agentConfig is present: replace the unconditional
required("aiCommand") with a conditional check that only requires a non-empty
aiCommand if state.get("agentConfig") is falsy (or drop the requirement entirely
if you prefer); ensure any existing error messages still use the required(...)
helper for consistency when the check is enforced.
source/src/story_automator/commands/tmux.py (1)

124-135: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Resolve the default agent after root is known.

agent_type() now returns a concrete provider, so these if agent == "auto" branches only run for an explicit --agent auto. In the default path, provider inference still happens before get_project_root() is available, which means invoking the wrapper from outside the repo can still pick the wrong runtime. Start with "auto"/the raw env value and resolve it with runtime_provider(root) once root is computed.

Also applies to: 174-178

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/src/story_automator/commands/tmux.py` around lines 124 - 135, The code
currently calls agent_type() and then later may override it with "--agent",
causing provider inference to run before get_project_root(); change the flow so
agent is initialized to the raw marker (e.g., "auto" or the raw env value)
instead of calling agent_type() up front, parse args to possibly set agent
string, then compute root = get_project_root() and only then, if agent == "auto"
(or equals the raw env placeholder), call runtime_provider(root) to get the
concrete provider; apply the same change to the second occurrence around the
block referenced at lines 174-178 (replace upfront agent_type() calls with the
raw marker and resolve via runtime_provider(root) after root is known).
🧹 Nitpick comments (2)
payload/.claude/skills/bmad-story-automator/data/tmux-commands.md (1)

80-80: ⚡ Quick win

Use consistent placeholder syntax and document the values.

The placeholder syntax mixes {project_root} (curly braces) with <installed-skill-root> (angle brackets), which is inconsistent with the rest of the document that uses curly braces for all placeholders (e.g., {epic}, {story_suffix}, {SESSION_NAME}).

Additionally, there's no explanation of what value users should substitute for <installed-skill-root>. Users won't know whether to use .claude/skills, .codex/skills, or something else.

Consider:

  1. Using consistent curly-brace syntax: {installed-skill-root}
  2. Adding a note explaining the expected values for Claude vs Codex, or
  3. Providing a shell variable example showing how to determine it dynamically
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@payload/.claude/skills/bmad-story-automator/data/tmux-commands.md` at line
80, Replace the inconsistent placeholder in the tmux command assignment (the
script variable name "script") by using {installed-skill-root} instead of
<installed-skill-root>, update the line to match the document's curly-brace
convention (e.g., "{project_root}/
{installed-skill-root}/bmad-story-automator/..."), and add a short note near
that line describing allowable values (for example: use ".claude/skills" for
Claude, ".codex/skills" for Codex) plus an optional shell example hint for users
to derive it dynamically (e.g., show how to detect the skill root via an
environment variable or a simple shell substitution).
source/src/story_automator/commands/state.py (1)

80-93: 💤 Low value

Defensive check on agent_config.get('defaultFallback') returning None.

agent_config.get('defaultFallback', agent_config.get('fallback', False)) only triggers the default when the key is absent. If a user (or earlier normalization) explicitly sets defaultFallback: null in the input config JSON, this renders as defaultFallback: null in the generated YAML, which agent_config.py's effective resolution logic may not treat as "disabled" the same way false is. Consider coercing None to False here for parity with the documented sentinel set (claude | codex | false).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/src/story_automator/commands/state.py` around lines 80 - 93, The
current assembly of lines uses agent_config.get('defaultFallback',
agent_config.get('fallback', False)) which leaves explicit null (None) values
through; update the logic that computes the defaultFallback value (located where
agent_config, per_task and lines are handled) to coerce None to False before
JSON-encoding — e.g., read fallback = agent_config.get('defaultFallback',
agent_config.get('fallback', False)); if fallback is None: fallback = False;
then use json.dumps(fallback) in the f-string — this ensures explicit null
becomes false to match agent_config.py's expected sentinel handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@payload/.claude/skills/bmad-story-automator/data/tmux-commands.md`:
- Around line 76-77: Revise the unclear line "Script: resolve the installed
helper under the active installed skill root." to explicitly state what
<installed-skill-root> means and give concrete examples per provider (e.g., for
Claude use ".claude/skills", for Codex use ".codex/skills"), and/or explain how
to determine it dynamically (e.g., describe checking the user's config or
environment variable that points to the active skill directory); update the
tmux-commands.md entry so readers know exactly which path to use or how to find
it rather than an ambiguous description.

In `@source/src/story_automator/commands/orchestrator_epic_agents.py`:
- Around line 243-254: The explicit-return paths in _resolve_primary_agent and
_resolve_fallback_agent currently return raw values un-normalized, causing
mismatches against downstream lowercase agent IDs; update both functions so the
non-runtime/false branches return the normalized, stripped, lowercase agent id
(e.g. use value = str(raw or "").strip().lower() and return value) or call the
shared resolver used elsewhere instead of returning str(raw), and ensure
_resolve_fallback_agent still maps boolean/false-like inputs to "false" while
normalizing explicit agent names to lowercase.

In `@source/src/story_automator/core/agent_config.py`:
- Around line 110-121: The resolvers return the raw string for non-auto values
which can preserve casing/whitespace and misroute agent selection; update
_resolve_primary_agent to return the already-normalized local variable (value)
instead of str(raw) and update _resolve_fallback_agent to return a normalized
string (e.g., str(value).strip().lower()) so both functions consistently return
stripped lowercase agent names; reference _resolve_primary_agent,
_resolve_fallback_agent, runtime_provider(), and normalize_fallback_value() when
making the change.

In `@source/src/story_automator/core/runtime_layout.py`:
- Around line 172-186: The current logic uses skills_root returned by
resolve_skills_root(STORY_SKILL_NAME) even when that was the synthetic fallback
(root/.claude/skills); change it to only prefer the skill-root-derived marker
when that skills_root actually exists/contains the story skill or was explicitly
selected: in runtime_layout.py, before using skills_root.relative_to(root) and
returning (skills_root.parent / ACTIVE_MARKER_NAME), add a guard that
skills_root.exists() (or another concrete existence check for the installed
story skill inside skills_root) and that the skill is not just the fallback
path; if the check fails, skip to the provider-based branches
(runtime_provider(root) == "codex" -> root/.agents/.story-automator-active, else
root/.claude/.story-automator-active).

---

Outside diff comments:
In `@source/src/story_automator/commands/state.py`:
- Around line 240-251: The validator currently calls required("aiCommand") and
flags empty strings even though aiCommand is deprecated; update the validation
in the state validation function (the block that calls required(...) and
load_policy_for_state) to make aiCommand optional when agentConfig is present:
replace the unconditional required("aiCommand") with a conditional check that
only requires a non-empty aiCommand if state.get("agentConfig") is falsy (or
drop the requirement entirely if you prefer); ensure any existing error messages
still use the required(...) helper for consistency when the check is enforced.

In `@source/src/story_automator/commands/tmux.py`:
- Around line 124-135: The code currently calls agent_type() and then later may
override it with "--agent", causing provider inference to run before
get_project_root(); change the flow so agent is initialized to the raw marker
(e.g., "auto" or the raw env value) instead of calling agent_type() up front,
parse args to possibly set agent string, then compute root = get_project_root()
and only then, if agent == "auto" (or equals the raw env placeholder), call
runtime_provider(root) to get the concrete provider; apply the same change to
the second occurrence around the block referenced at lines 174-178 (replace
upfront agent_type() calls with the raw marker and resolve via
runtime_provider(root) after root is known).

---

Nitpick comments:
In `@payload/.claude/skills/bmad-story-automator/data/tmux-commands.md`:
- Line 80: Replace the inconsistent placeholder in the tmux command assignment
(the script variable name "script") by using {installed-skill-root} instead of
<installed-skill-root>, update the line to match the document's curly-brace
convention (e.g., "{project_root}/
{installed-skill-root}/bmad-story-automator/..."), and add a short note near
that line describing allowable values (for example: use ".claude/skills" for
Claude, ".codex/skills" for Codex) plus an optional shell example hint for users
to derive it dynamically (e.g., show how to detect the skill root via an
environment variable or a simple shell substitution).

In `@source/src/story_automator/commands/state.py`:
- Around line 80-93: The current assembly of lines uses
agent_config.get('defaultFallback', agent_config.get('fallback', False)) which
leaves explicit null (None) values through; update the logic that computes the
defaultFallback value (located where agent_config, per_task and lines are
handled) to coerce None to False before JSON-encoding — e.g., read fallback =
agent_config.get('defaultFallback', agent_config.get('fallback', False)); if
fallback is None: fallback = False; then use json.dumps(fallback) in the
f-string — this ensures explicit null becomes false to match agent_config.py's
expected sentinel handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 40da1dbc-d90a-4eb0-b96c-852fac4e119a

📥 Commits

Reviewing files that changed from the base of the PR and between 01f412d and fdfc310.

📒 Files selected for processing (27)
  • README.md
  • docs/cli-reference.md
  • docs/how-it-works.md
  • docs/installation-and-layout.md
  • docs/troubleshooting.md
  • install.sh
  • payload/.claude/skills/bmad-story-automator-review/instructions.xml
  • payload/.claude/skills/bmad-story-automator/data/agent-fallback-troubleshooting.md
  • payload/.claude/skills/bmad-story-automator/data/agent-fallback.md
  • payload/.claude/skills/bmad-story-automator/data/code-review-loop.md
  • payload/.claude/skills/bmad-story-automator/data/orchestration-policy.json
  • payload/.claude/skills/bmad-story-automator/data/preflight-prompts.md
  • payload/.claude/skills/bmad-story-automator/data/retrospective-automation.md
  • payload/.claude/skills/bmad-story-automator/data/retrospective-prompts.md
  • payload/.claude/skills/bmad-story-automator/data/stop-hook-config.md
  • payload/.claude/skills/bmad-story-automator/data/stop-hook-troubleshooting.md
  • payload/.claude/skills/bmad-story-automator/data/tmux-commands.md
  • payload/.claude/skills/bmad-story-automator/data/workflow-commands.md
  • payload/.claude/skills/bmad-story-automator/templates/state-document.md
  • scripts/smoke-test.sh
  • source/src/story_automator/commands/orchestrator_epic_agents.py
  • source/src/story_automator/commands/state.py
  • source/src/story_automator/commands/tmux.py
  • source/src/story_automator/core/agent_config.py
  • source/src/story_automator/core/runtime_layout.py
  • source/src/story_automator/core/tmux_runtime.py
  • source/tests/test_runtime_layout.py
✅ Files skipped from review due to trivial changes (8)
  • payload/.claude/skills/bmad-story-automator/data/code-review-loop.md
  • docs/cli-reference.md
  • payload/.claude/skills/bmad-story-automator/data/agent-fallback-troubleshooting.md
  • docs/how-it-works.md
  • payload/.claude/skills/bmad-story-automator/data/stop-hook-config.md
  • docs/installation-and-layout.md
  • payload/.claude/skills/bmad-story-automator/data/stop-hook-troubleshooting.md
  • README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/troubleshooting.md
  • payload/.claude/skills/bmad-story-automator/data/retrospective-automation.md
  • source/tests/test_runtime_layout.py

Comment thread payload/.claude/skills/bmad-story-automator/data/tmux-commands.md Outdated
Comment thread source/src/story_automator/commands/orchestrator_epic_agents.py Outdated
Comment thread source/src/story_automator/core/agent_config.py Outdated
@dickymoore
Copy link
Copy Markdown
Author

Review follow-up summary:

  • Fixed runtime marker fallback, explicit agent normalization, installed-root docs, null fallback handling, and tmux root-aware agent selection in 6ebbd06 and 5580dc2.
  • Confirmed existing fixes for non-dict perTask, portable path escaping, and agentConfig-only state validation are already on the branch.
  • Deferred cleanup/cosmetic stop-hook items are listed in the PR description under Proposed Follow-ups item 8; the bespoke frontmatter parser follow-up remains item 7.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
docs/installation-and-layout.md (3)

22-33: ⚡ Quick win

Clarify when to use .agents/skills vs provider-specific roots.

The documentation lists three supported roots but doesn't explain when operators should use the generic .agents/skills versus provider-specific roots like .codex/skills. Consider adding a brief note about:

  • Is .agents/skills a recommended generic location for multi-provider projects?
  • When should operators prefer provider-specific roots?
  • Does the runtime auto-detect across all three locations?

This would help operators make informed layout decisions during setup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/installation-and-layout.md` around lines 22 - 33, Add a short clarifying
paragraph to the installation-and-layout doc explaining that .agents/skills is
the recommended generic root for multi-provider or provider-agnostic projects,
provider-specific roots like .codex/skills or .claude/skills should be used when
you need provider-specific config or isolation, and that the runtime
auto-detects and merges across all supported roots (".agents/skills",
".claude/skills", ".codex/skills") so the installer will update every qualifying
root; also mention that payload/.claude/skills is only an internal packaged path
and not part of runtime hierarchy.

22-75: ⚡ Quick win

Consider defining skill root terminology upfront.

The document uses several related terms throughout:

  • "supported skill root" / "supported dependency skill root" (lines 22, 29, 53)
  • "qualifying root" / "qualifying skill root" (lines 31, 75)
  • "dependency skill root" (line 14)
  • "installed skill root" (line 39)
  • "target root" (line 33)

While context makes the meaning clear, explicitly defining the relationship between these terms early in the document would help operators understand the install logic:

  • Supported roots: The three recognized locations (.agents/skills, .claude/skills, .codex/skills)
  • Qualifying roots: Supported roots that contain all required dependency SKILL.md files
  • Installed skill root: The qualifying root where bmad-story-automator gets installed

This is a low-priority documentation polish item.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/installation-and-layout.md` around lines 22 - 75, Add a short
"Terminology" section near the top that defines the terms used in the doc: list
"Supported roots" and enumerate `.agents/skills`, `.claude/skills`,
`.codex/skills`; define "Qualifying roots" as supported roots that contain all
required dependency SKILL.md files; define "Installed skill root" / "Target
root" as the qualifying root(s) where bmad-story-automator is copied; and
clarify "Installed skill payload" as the internal payload path under
payload/.claude/skills. Then update usages of the phrases across the file to
match these canonical names (e.g., replace mixed uses of "supported dependency
skill root", "dependency skill root", "qualifying root", "installed skill root",
and "target root" with the chosen terms) so terminology is consistent.

102-103: 💤 Low value

Consider clarifying "still uses" phrasing.

The phrase "Codex still uses the active installed skill root" might be clearer as "Codex uses the active installed skill root for the helper entrypoint, but writes hook/config state..."

The word "still" suggests a contrast with previous behavior, but the contrast here is between skill location and config location, not temporal change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/installation-and-layout.md` around lines 102 - 103, Update the sentence
to remove ambiguous "still uses" phrasing: replace "Codex still uses the active
installed skill root for this helper, but writes hook/config state to
`.codex/hooks.json` and `.codex/config.toml`." with wording like "Codex uses the
active installed skill root for the helper entrypoint, but writes hook/config
state to `.codex/hooks.json` and `.codex/config.toml`." to clarify the contrast
is between skill location and config location; target the sentence containing
"Codex still uses the active installed skill root" in
docs/installation-and-layout.md.
source/tests/test_runtime_layout.py (1)

191-197: ⚡ Quick win

_install_bundle will fail with an opaque FileNotFoundError if the payload is absent

shutil.copytree raises FileNotFoundError directly if payload/.claude/skills/bmad-story-automator or bmad-story-automator-review doesn't exist. Because every test that exercises provider-detection or marker logic depends on this helper, the failure surfaces as a cryptic OS error with no hint about the missing payload, rather than a skipped or clearly-labelled test failure.

A minimal guard keeps the intent clear and prevents confusion in stripped-down checkouts or isolated package installs:

🛠️ Proposed fix
 def _install_bundle(self, runtime_dir: str) -> None:
     source_skill = REPO_ROOT / "payload" / ".claude" / "skills" / "bmad-story-automator"
     source_review = REPO_ROOT / "payload" / ".claude" / "skills" / "bmad-story-automator-review"
+    if not source_skill.is_dir() or not source_review.is_dir():
+        self.skipTest(f"payload skills not found under {REPO_ROOT / 'payload'}")
     target_root = self.project_root / runtime_dir / "skills"
     target_root.mkdir(parents=True, exist_ok=True)
     shutil.copytree(source_skill, target_root / "bmad-story-automator")
     shutil.copytree(source_review, target_root / "bmad-story-automator-review")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/tests/test_runtime_layout.py` around lines 191 - 197, The helper
_install_bundle currently calls shutil.copytree on source_skill and
source_review and will raise an opaque FileNotFoundError if the payload is
missing; update _install_bundle to check existence of REPO_ROOT / "payload" /
".claude" / "skills" for both source_skill and source_review before calling
shutil.copytree and raise a clearer, descriptive error (or call pytest.skip)
that mentions the missing payload path and test dependency (reference symbols:
_install_bundle, source_skill, source_review, shutil.copytree, REPO_ROOT,
project_root) so failures in test_runtime_layout are explicit and informative.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@source/src/story_automator/commands/state.py`:
- Around line 88-95: The emitted defaultPrimary can become JSON null if the key
exists with null; mirror the defaultFallback guard by computing a
default_primary variable: retrieve agent_config.get('defaultPrimary') into
default_primary, and if it is None set it to agent_config.get('primary', 'auto')
(or 'auto' if that is missing); then use default_primary in the f-string that
emits the "defaultPrimary" line so you never serialize a Python None to "null".

In `@source/src/story_automator/commands/tmux.py`:
- Line 291: cmd_monitor_session currently only treats "auto" specially and
leaves "runtime" as a literal, causing session_status to receive wrong provider
flags; replace the ad-hoc check in cmd_monitor_session (where agent is read and
tested with if agent == "auto") with the same helper used elsewhere: call
_resolve_agent_selection(...) to normalize the agent and derive the
codex/runtime/provider flags (same approach used by _spawn and _build_cmd) and
then pass the normalized values into session_status; ensure the agent variable
used in session_status calls is the resolved value returned by
_resolve_agent_selection so "runtime" is handled correctly.

---

Nitpick comments:
In `@docs/installation-and-layout.md`:
- Around line 22-33: Add a short clarifying paragraph to the
installation-and-layout doc explaining that .agents/skills is the recommended
generic root for multi-provider or provider-agnostic projects, provider-specific
roots like .codex/skills or .claude/skills should be used when you need
provider-specific config or isolation, and that the runtime auto-detects and
merges across all supported roots (".agents/skills", ".claude/skills",
".codex/skills") so the installer will update every qualifying root; also
mention that payload/.claude/skills is only an internal packaged path and not
part of runtime hierarchy.
- Around line 22-75: Add a short "Terminology" section near the top that defines
the terms used in the doc: list "Supported roots" and enumerate
`.agents/skills`, `.claude/skills`, `.codex/skills`; define "Qualifying roots"
as supported roots that contain all required dependency SKILL.md files; define
"Installed skill root" / "Target root" as the qualifying root(s) where
bmad-story-automator is copied; and clarify "Installed skill payload" as the
internal payload path under payload/.claude/skills. Then update usages of the
phrases across the file to match these canonical names (e.g., replace mixed uses
of "supported dependency skill root", "dependency skill root", "qualifying
root", "installed skill root", and "target root" with the chosen terms) so
terminology is consistent.
- Around line 102-103: Update the sentence to remove ambiguous "still uses"
phrasing: replace "Codex still uses the active installed skill root for this
helper, but writes hook/config state to `.codex/hooks.json` and
`.codex/config.toml`." with wording like "Codex uses the active installed skill
root for the helper entrypoint, but writes hook/config state to
`.codex/hooks.json` and `.codex/config.toml`." to clarify the contrast is
between skill location and config location; target the sentence containing
"Codex still uses the active installed skill root" in
docs/installation-and-layout.md.

In `@source/tests/test_runtime_layout.py`:
- Around line 191-197: The helper _install_bundle currently calls
shutil.copytree on source_skill and source_review and will raise an opaque
FileNotFoundError if the payload is missing; update _install_bundle to check
existence of REPO_ROOT / "payload" / ".claude" / "skills" for both source_skill
and source_review before calling shutil.copytree and raise a clearer,
descriptive error (or call pytest.skip) that mentions the missing payload path
and test dependency (reference symbols: _install_bundle, source_skill,
source_review, shutil.copytree, REPO_ROOT, project_root) so failures in
test_runtime_layout are explicit and informative.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b9b8c605-a193-445f-ac4f-87b50d56123c

📥 Commits

Reviewing files that changed from the base of the PR and between fdfc310 and df3131c.

📒 Files selected for processing (14)
  • README.md
  • docs/installation-and-layout.md
  • install.sh
  • payload/.claude/skills/bmad-story-automator/data/orchestration-policy.json
  • payload/.claude/skills/bmad-story-automator/data/tmux-commands.md
  • scripts/smoke-test.sh
  • source/src/story_automator/commands/orchestrator_epic_agents.py
  • source/src/story_automator/commands/state.py
  • source/src/story_automator/commands/tmux.py
  • source/src/story_automator/core/agent_config.py
  • source/src/story_automator/core/runtime_layout.py
  • source/src/story_automator/core/runtime_policy.py
  • source/tests/test_runtime_layout.py
  • source/tests/test_state_policy_metadata.py
🚧 Files skipped from review as they are similar to previous changes (9)
  • payload/.claude/skills/bmad-story-automator/data/orchestration-policy.json
  • payload/.claude/skills/bmad-story-automator/data/tmux-commands.md
  • source/src/story_automator/core/agent_config.py
  • install.sh
  • scripts/smoke-test.sh
  • source/src/story_automator/commands/orchestrator_epic_agents.py
  • README.md
  • source/tests/test_state_policy_metadata.py
  • source/src/story_automator/core/runtime_layout.py

Comment thread skills/bmad-story-automator/src/story_automator/commands/state.py Outdated
Comment thread skills/bmad-story-automator/src/story_automator/commands/tmux.py Outdated
@dickymoore dickymoore changed the title Add Codex runtime support feat: add Codex runtime support May 8, 2026
@dickymoore dickymoore force-pushed the dicky/codex-runtime-support branch from 0217624 to cf96221 Compare May 8, 2026 17:29
Copy link
Copy Markdown
Contributor

@bma-d bma-d left a comment

Choose a reason for hiding this comment

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

Review pass complete. P0 clean, P1 not clean. I focused on preserving existing Claude behavior and keeping runtime/provider concerns separated from child-agent selection. Two items could not be placed inline because the affected files/lines are not in this PR diff: docs/state-and-resume.md still hard-codes .claude/.story-automator-active, and steps-v/step-v-01-check.md still treats deprecated aiCommand as the only valid runtime command field even though runtime validation accepts agentConfig. Those should still be fixed before merge.

The main architectural ask: split runtime context from child-agent selection. Provider/layout, skill root, marker root, hook target, and child agent should be separate values; AI_AGENT should not decide hook/config layout.

# Provider controls hook/config syntax: Claude writes settings.json, Codex writes
# hooks.json plus config.toml. Marker paths are resolved separately so they can
# follow the installed skill root in mixed or migrated workspaces.
for name in ("BMAD_RUNTIME_PROVIDER", "STORY_AUTOMATOR_RUNTIME_PROVIDER", "AI_AGENT"):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: AI_AGENT is a child-agent selector, but runtime_provider() also treats it as the top-level hook/config provider. That means a Claude-installed project can export AI_AGENT=codex for fallback/worker selection and then ensure-stop-hook writes .codex/hooks.json instead of .claude/settings.json, losing the existing Claude stop-hook safeguard.

Suggested fix: remove AI_AGENT from provider/layout detection. Use only explicit provider env such as BMAD_RUNTIME_PROVIDER / STORY_AUTOMATOR_RUNTIME_PROVIDER, installed skill root/session metadata, or a dedicated runtime context object for hook/config layout. Keep AI_AGENT scoped to spawned child sessions. Add a regression for .claude/skills/bmad-story-automator plus AI_AGENT=codex still writing .claude/settings.json.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in b3a4c9e: runtime_provider() no longer reads AI_AGENT, so child-agent selection no longer changes top-level hook/config layout. Added regression coverage for a Claude-installed project with AI_AGENT=codex still resolving Claude marker/provider behavior and still writing Claude hook config.

}


def ensure_codex_stop_hook(*, project_root: Path, command: str, timeout: int) -> dict[str, Any]:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Codex hook installation writes .codex/config.toml and .codex/hooks.json, then later reports Codex Stop hook verified. without checking whether Codex actually trusts/runs the unmanaged hook. Current Codex hook state exposes trust/hash state, so a configured-but-untrusted hook can look verified here while the stop safeguard is inactive.

Suggested fix: after writing Codex hook config, query/inspect Codex hook state for the project and match the Story Automator Stop hook. If the hook is unmanaged and not trusted, return a pending-trust state instead of verified, with an explicit approval instruction. If you add an approval path, make it a separate user-approved trust step, not an automatic write hidden inside install.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in b3a4c9e: Codex stop-hook setup now distinguishes configured vs pending-trust vs verified state instead of always implying the safeguard is active. ensure_codex_stop_hook() now returns trust-aware status fields/messages, and coverage locks the configured-but-untrusted path.

config.default_primary = data.get("defaultPrimary") or data.get("primary") or "claude"
config.default_fallback = data.get("defaultFallback") or data.get("fallback") or "codex"
config.default_primary = data.get("defaultPrimary") or data.get("primary") or "auto"
config.default_fallback = data.get("defaultFallback") or data.get("fallback") or "false"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: This changes the missing/legacy fallback default from Codex to disabled fallback. On automator/main, empty config and {defaultPrimary:"claude"} resolved to Claude primary plus Codex fallback; this PR resolves both to no fallback. That breaks existing Claude-first retry behavior unless every existing state/config explicitly opts back in.

Suggested fix: preserve the legacy fallback default for missing/unversioned config, or introduce an explicit config version/migration where only new-version configs default defaultFallback to false. Add tests for {} and {"defaultPrimary":"claude"} so the compatibility choice is locked.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in b3a4c9e: missing/unversioned fallback config now preserves the legacy Codex fallback instead of silently disabling fallback. Added targeted regressions for {} and {"defaultPrimary":"claude"} plus explicit-disabled fallback coverage.

Comment thread docs/installation-and-layout.md Outdated
- `.codex/skills`

Unlike the older workflow-root layout, this Python port installs into the pure skill tree.
Use `.agents/skills` for provider-agnostic or multi-provider projects. Use `.claude/skills` or `.codex/skills` when provider-specific isolation is useful. For Codex, installed skill assets live under a supported root such as `.agents/skills` or `.codex/skills`, while stop-hook and runtime config files are managed under `.codex/`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The docs describe .agents/skills as provider-agnostic/multi-provider, but runtime inference treats .agents as Codex. That separation matters: installing into a supposedly neutral root can change hook/config semantics to Codex.

Suggested fix: either make .agents truly provider-neutral and require explicit runtime provider context for hook/config writes, or document .agents as Codex-owned. I prefer the former so Codex support does not alter Claude behavior in mixed installs.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Partially addressed in b3a4c9e: this pass documents the corrected provider resolution model and explicitly separates provider/layout detection from AI_AGENT. I did not redesign .agents to be provider-neutral in this PR; that broader model change is intentionally deferred to a follow-up if we decide to pursue it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated in 05dad8c: the installation/layout doc now distinguishes install-root behavior from runtime provider/layout behavior instead of describing .agents/skills as simply provider-agnostic. It now states the current runtime fallback model explicitly: explicit provider env wins first, active skill-root context comes next, and .agents / .codex fallback to Codex-style hook/config layout while .claude falls back to Claude-style layout.

Comment thread docs/installation-and-layout.md Outdated
Unlike the older workflow-root layout, this Python port installs into the pure skill tree.
Use `.agents/skills` for provider-agnostic or multi-provider projects. Use `.claude/skills` or `.codex/skills` when provider-specific isolation is useful. For Codex, installed skill assets live under a supported root such as `.agents/skills` or `.codex/skills`, while stop-hook and runtime config files are managed under `.codex/`.

There is no runtime precedence among these roots. Claude-only, Codex-only, and mixed projects are supported. If more than one root contains all required dependency `SKILL.md` files, the installer updates each qualifying root. If a root is present but missing required skill entrypoints while another root qualifies, the incomplete root is left unchanged.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: This says there is no runtime precedence among roots, but the resolver has a real order: explicit BMAD_SKILLS_ROOT, current installed helper root, project .agents, project .claude, project .codex, then home roots. Operators need the documented rule to predict marker path, asset resolution, and contract resolution in mixed installs.

Suggested fix: document the actual precedence, or make mixed-root execution fail/ask for an explicit root/provider. Add a mixed-root test that enforces whichever rule you document.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Partially addressed in b3a4c9e: the doc now records the provider-resolution order used by the merge-blocker fix and makes AI_AGENT non-authoritative for provider/layout selection. I did not expand this into a broader mixed-root execution redesign in this PR; any deeper precedence/model cleanup stays in the follow-up bucket.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated in 05dad8c: the doc no longer says there is no runtime precedence. It now separates installer behavior from runtime behavior and documents the active runtime resolution order (BMAD_SKILLS_ROOT, current helper root, project .agents, .claude, .codex, then home roots).

Comment thread tests/test_runtime_layout.py Outdated
self._install_bundle(".agents")

self.assertEqual(runtime_provider(str(self.project_root)), "codex")
self.assertEqual(active_marker_path(self.project_root), self.project_root / ".agents" / ".story-automator-active")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: These assertions compare resolved runtime paths against unresolved TemporaryDirectory() paths. On macOS, /var resolves through /private/var, so npm run test:python fails locally with 7 runtime-layout path mismatches even though the behavior under test is otherwise the same.

Suggested fix: compare resolved expected paths, e.g. (self.project_root / ".agents" / ".story-automator-active").resolve(), or normalize both sides in a small helper. Keep separate string-entry tests for unresolved project-relative display values like .agents/.story-automator-active.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Leaving this one deferred on purpose. The current merge-blocker pass stayed focused on runtime/provider correctness and stop-hook safety; macOS temp-path normalization is still planned as a tightly-scoped follow-up test-robustness PR.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Folded back into the main PR in 05dad8c: the runtime-layout tests now compare normalized/resolved path targets so the targeted Python test subset is portable across macOS /var vs /private/var path layouts as well as Linux.

Comment thread docs/cli-reference.md Outdated

```bash
scripts=".claude/skills/bmad-story-automator/scripts/story-automator"
scripts="<installed-skill-root>/bmad-story-automator/scripts/story-automator"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: This is now a copy-paste command, but <installed-skill-root> is a literal placeholder and will fail if operators paste it during setup or incident debugging.

Suggested fix: include a tiny root-discovery snippet over .agents/skills, .claude/skills, and .codex/skills, then set scripts from the discovered executable. Mirror the same pattern in troubleshooting examples.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Deferring this intentionally to the docs/operator-ergonomics follow-up PR. I kept the blocker pass limited to runtime/provider correctness and only updated the contract-critical docs needed for those behavior changes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Folded back into the main PR in 05dad8c: the CLI reference is now copy-paste-safe. It resolves the installed helper from supported skill roots first and then uses $scripts in the examples instead of the literal <installed-skill-root> placeholder.

Comment thread docs/troubleshooting.md Outdated
.claude/skills/bmad-story-automator/scripts/story-automator tmux-wrapper list --project-only
.claude/skills/bmad-story-automator/scripts/story-automator orchestrator-helper state-list _bmad-output/story-automator
.claude/skills/bmad-story-automator/scripts/story-automator orchestrator-helper verify-code-review 1.2
<installed-skill-root>/bmad-story-automator/scripts/story-automator tmux-wrapper list --project-only
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: Same placeholder issue as the CLI reference: these incident-response checks are likely to be copy-pasted, and <installed-skill-root> is not executable.

Suggested fix: define scripts once using documented root discovery, then use $scripts tmux-wrapper list --project-only, $scripts orchestrator-helper state-list ..., and $scripts orchestrator-helper verify-code-review ....

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Deferring this intentionally to the docs/operator-ergonomics follow-up PR alongside the CLI reference placeholder cleanup so both operator-facing command examples can be fixed together.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Folded back into the main PR in 05dad8c: the troubleshooting guide now uses the same supported-root discovery snippet and $scripts examples instead of the literal <installed-skill-root> placeholder.

if os.environ.get("STORY_AUTOMATOR_CHILD", "").lower() == "true":
return 0
marker = Path(os.getcwd()) / ".claude" / ".story-automator-active"
marker = active_marker_path(Path(os.getcwd()))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The stop hook discards the hook payload and resolves the marker from os.getcwd(). The Codex hook config stores only the command, with no project-root env. If Codex invokes the hook from a subdirectory, this checks <subdir>/.agents|.codex/.story-automator-active instead of the project-root marker and can silently allow stopping during an active orchestration.

Suggested fix: pass/export the project root in the installed hook command, or parse the hook stdin payload for cwd/session context and normalize to the project root deterministically before calling active_marker_path(). Add a test where the marker is at project root but the hook process cwd is a nested directory.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in b3a4c9e: installed stop-hook commands now export PROJECT_ROOT, and the hook resolves the active marker from that project root instead of ambient cwd. Added regression coverage for nested-directory invocation with a project-root marker.

agent = agent or _raw_agent_selection()
story_prefix = story_id.replace(".", "-")
root = get_project_root()
agent = _resolve_agent_selection(agent, root)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Legacy AI_COMMAND is honored after auto has already resolved through runtime_provider(). In an .agents/Codex install with AI_COMMAND="claude ..." and no AI_AGENT, build-cmd emits a Claude command while spawn/monitor classify the session as Codex. That skips Claude-specific completion handling and can leave a finished Claude child stuck or timed out.

Suggested fix: when AI_COMMAND is present without AI_AGENT, infer the agent from the command before resolving/spawning, or force the whole legacy path to Claude consistently. Add a regression that AI_COMMAND=claude ... records/monitors the session as Claude even in a Codex-layout install.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in b3a4c9e: when legacy AI_COMMAND is present without AI_AGENT, build/monitor behavior now infers the agent from the command instead of classifying the session through the Codex layout by default. Added regression coverage for a Claude legacy command path.

@dickymoore
Copy link
Copy Markdown
Author

Merge-blocker rework is now pushed on dicky/codex-runtime-support at b3a4c9e.

Addressed in this pass:

  • runtime/provider layout no longer reads AI_AGENT; child-agent selection is now separate from top-level hook/config behavior
  • legacy missing/unversioned fallback config preserves the prior Codex fallback instead of silently disabling fallback
  • Codex stop-hook setup now reports configured vs pending-trust vs verified instead of always implying the safeguard is active
  • installed stop-hook commands export PROJECT_ROOT so marker resolution does not depend on the caller's working directory
  • legacy AI_COMMAND classification is now consistent with build/monitor behavior
  • contract-critical docs and validation guidance were updated to match the corrected runtime model

Validation run for this pass:

  • PYTHONPATH=skills/bmad-story-automator/src python3 -m pytest tests/test_runtime_layout.py tests/test_stop_hooks.py tests/test_state_policy_metadata.py tests/test_success_verifiers.py -q
  • result: 126 passed

Intentionally deferred to tightly-scoped follow-up PRs:

  1. CLI/troubleshooting doc copy-paste ergonomics
  2. remaining state/validation doc alignment
  3. test robustness cleanup
  4. low-risk stop-hook/internal cleanup

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants