feat(makefile): plugin execution loop and JSON aggregation (Story 13.5)#38
Merged
matthew-on-git merged 2 commits intomainfrom May 5, 2026
Merged
feat(makefile): plugin execution loop and JSON aggregation (Story 13.5)#38matthew-on-git merged 2 commits intomainfrom
matthew-on-git merged 2 commits intomainfrom
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Story 13.5 — final implementation story before the v1.10.0 plugin-architecture marketing release.
_lint/_format/_fix/_test/_security. Each recipe sourceslib/plugin-execute.shand callsdispatch_plugin_targetafter the lastHAS_<LANG>block. Plugin results aggregate into the existingran_languages/failed_languages/ JSON envelope — consumers cannot distinguish plugin vs core results from the JSON shape.plugin gate skippedevent.{paths}interpolation frompaths_var, filtered to existing paths..devrail.ymlentries likeelixir: { linter: dialyxir }replacetargets.<name>.cmd.DEVRAIL_FAIL_FAST=1parity — short-circuits dispatcher and recipe.plugins:absent — v1.9.x consumers see byte-identical JSON output.SHELL := /bin/bashso recipes can source bash-only libs.tests/test-plugin-execution.sh+ new CI step.Closes Story 13.5 (Epic 13).
Test plan
make checkon dev-toolchain itself (no plugins) — JSON unchanged from v1.10.4 baselinetests/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)/bmad-bmm-code-review) — recommend a different model than the implementer🤖 Generated with Claude Code