Skip to content

feat(makefile): plugin execution loop and JSON aggregation (Story 13.5)#38

Merged
matthew-on-git merged 2 commits intomainfrom
feat/13-5-plugin-execution-loop
May 5, 2026
Merged

feat(makefile): plugin execution loop and JSON aggregation (Story 13.5)#38
matthew-on-git merged 2 commits intomainfrom
feat/13-5-plugin-execution-loop

Conversation

@matthew-on-git
Copy link
Copy Markdown
Contributor

Summary

Story 13.5 — final implementation story before the v1.10.0 plugin-architecture marketing release.

  • Wires loaded plugins into _lint/_format/_fix/_test/_security. Each recipe sources lib/plugin-execute.sh and calls dispatch_plugin_target after the last HAS_<LANG> block. Plugin results aggregate into the existing ran_languages / failed_languages / JSON envelope — consumers cannot distinguish plugin vs core results from the JSON shape.
  • Per-target gate evaluation (file / dir / glob match; absolute paths rejected). Skips emit a structured plugin gate skipped event.
  • {paths} interpolation from paths_var, filtered to existing paths.
  • Per-language overrides for plugin languages: .devrail.yml entries like elixir: { linter: dialyxir } replace targets.<name>.cmd.
  • DEVRAIL_FAIL_FAST=1 parity — short-circuits dispatcher and recipe.
  • No-op when plugins: absent — v1.9.x consumers see byte-identical JSON output.
  • SHELL := /bin/bash so recipes can source bash-only libs.
  • 10-case smoke test at tests/test-plugin-execution.sh + new CI step.

Closes Story 13.5 (Epic 13).

Test plan

  • make check on dev-toolchain itself (no plugins) — JSON unchanged from v1.10.4 baseline
  • tests/test-plugin-execution.sh — 10/10 pass (dispatcher dispatches, gates skip / run, paths interpolated, overrides applied, fail-fast short-circuits, partial targets respected, no-op JSON identical to v1.10.x baseline)
  • tests/test-plugin-loader.sh — regression-safe (still 100%)
  • tests/test-plugin-resolver.sh — regression-safe (16/16)
  • CI green
  • Code review (/bmad-bmm-code-review) — recommend a different model than the implementer

🤖 Generated with Claude Code

matthew-on-git and others added 2 commits May 4, 2026 20:39
Wires loaded plugins into the existing _lint/_format/_fix/_test/_security
recipes via a new lib/plugin-execute.sh. Each recipe sources the lib at
its top, then calls dispatch_plugin_target after the last HAS_<LANG>
block; plugin results aggregate into the same ran_languages /
failed_languages / JSON envelope as core results.

What ships:

- lib/plugin-execute.sh — sourceable helpers (evaluate_gate, render_cmd,
  apply_override, dispatch_plugin_target). Reads from the loader cache
  populated by Story 13.2; uses log_event from lib/log.sh; runs cmds via
  bash -c (no eval).

- Per-target gate evaluation (file / dir / glob match; absolute paths
  rejected with structured error). Empty / missing gate = always run.

- {paths} interpolation from paths_var, filtered to existing paths
  (mirrors the RUBY_PATHS pattern in core).

- Per-language overrides for plugin languages: .devrail.yml entries
  like `elixir: { linter: dialyxir }` replace targets.<name>.cmd. Map:
  lint→linter, format_check/format_fix→formatter, fix→fixer, test→test,
  security→security.

- DEVRAIL_FAIL_FAST=1 short-circuits the dispatcher and the recipe.

- No-op when plugins: is absent — v1.9.x consumers see byte-identical
  JSON output.

- SHELL := /bin/bash so recipes can source bash-only libs (plugin-
  execute.sh uses [[, ((, indirect parameter expansion). Existing POSIX
  recipes remain valid.

- tests/test-plugin-execution.sh — 10 cases exercising no-op, pass,
  fail, gate skip / run, paths interpolation, override, fail-fast,
  partial targets, JSON regression. New CI step "Plugin execution
  smoke test" added to .github/workflows/ci.yml.

Closes Story 13.5 (Epic 13). Final implementation story before the
v1.10.0 marketing release (Story 13.6).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
H1: render_cmd no longer calls `exit 2` from the sourced library — that
    killed the recipe before the JSON envelope emitted. Helpers now return
    non-zero, dispatcher catches them, marks the plugin failed, and lets
    the recipe continue (Case 12 verifies the second plugin still runs).

M1: evaluate_gate now returns 0/1/2 distinctly. Gate-skip (path missing)
    is silent; absolute-path config error surfaces as plugin failure with
    a `<plugin>:gate-config` entry in failed_languages (Case 11).

M2: yq parse errors on the loader cache are no longer swallowed by
    `2>/dev/null`. A malformed cache produces a structured error event
    and a `_plugins:cache-parse` plugin-system failure (Case 15).

M3: cache + .devrail.yml are converted to JSON once at the start of each
    dispatch and re-used via jq for all per-plugin lookups. Earlier path
    invoked yq ~8N times per recipe; new path is 2 yq calls + ~3N jq
    calls (each jq invocation on the cached JSON is much faster than yq
    on the YAML file).

M4: optional DEVRAIL_PLUGIN_TIMEOUT_SECONDS env wraps each plugin cmd
    in `timeout -k 5 N bash -c …`. Unset = no timeout (default).

M5: _fix dispatches both `format_fix` AND `fix` so plugins that declare
    `targets.fix.cmd:` per the schema actually run.

M6: render_cmd's filter loop rejects path entries containing shell-meta
    characters (`;|&$<>(){}\` etc.) with a structured warn event. Closes
    the path-with-`;` injection vector through `bash -c "${final_cmd}"`
    (Case 13 inserts a `lib;evil` directory and asserts it's filtered).

L1: caller-scope vars registered with shellcheck via a single
    `:` no-op assignment at the top of dispatch_plugin_target. Drops 4
    scattered SC2034 disables.

L2: 5 new test cases (11-15) cover absolute-path gate, {paths} without
    paths_var, shell-meta path rejection, triple-source idempotency,
    and malformed-cache parse error.

L3: Case 9 (plugin without 'test' target) tightens silent-skip
    assertion — now rejects ANY plugin event for the absent target.

L4: STABILITY.md notes the Makefile contract pins SHELL := /bin/bash
    as of v1.10.x for consumer template repos that inherit it.

L5: dispatcher appends gate-skipped plugins to skipped_languages so
    _test/_security recipes that maintain that array stay consistent
    with how core "no work to do" cases are reported.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@matthew-on-git matthew-on-git merged commit f7aaf70 into main May 5, 2026
3 checks passed
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.

1 participant