Skip to content

feat: plugin ecosystem + step.workflow_call#331

Merged
intel352 merged 14 commits intomainfrom
feat/plugin-ecosystem-engine
Mar 15, 2026
Merged

feat: plugin ecosystem + step.workflow_call#331
intel352 merged 14 commits intomainfrom
feat/plugin-ecosystem-engine

Conversation

@intel352
Copy link
Contributor

Summary

  • Plugin ecosystem enhancements — auto-fetch external plugins on startup, binary integrity verification (SHA-256 checksums from lockfile), enhanced wfctl plugin install (URL, local, static registry sources), comprehensive plugin scaffold generator
  • step.workflow_call — new pipeline step for cross-pipeline dispatch. Enables a pipeline to invoke another pipeline by name with full context forwarding. Template-resolved workflow names supported. Required for WebSocket message routing in multi-pipeline applications (e.g., card game platform).
  • Plugin authoring guide — comprehensive documentation for building external plugins

Key Changes

step.workflow_call

Calls another pipeline by name, forwarding trigger data and current context:

- name: dispatch
  type: step.workflow_call
  config:
    workflow: "pipeline:{{ .body.type }}"
    stop_pipeline: true

Plugin auto-fetch

Declared external plugins in config are automatically downloaded on engine startup if not present locally.

Binary integrity

Plugin binaries verified against SHA-256 checksums in lockfile on load.

wfctl enhancements

  • plugin install --url for direct URL installs
  • plugin install --local for local directory installs
  • Static registry source type for GitHub Pages
  • Enhanced scaffold with full project structure

Test plan

  • go test ./... passes
  • step.workflow_call tested with template resolution + stop_pipeline
  • Auto-fetch, integrity, and config parsing tests added
  • URL/local install, checksums, StaticRegistry, scaffold tests added
  • Integration-tested via card game platform (7 games, all passing e2e)

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings March 15, 2026 05:33
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds support for declaring external plugins in workflow config, auto-fetching them via wfctl, and verifying installed plugin binaries against pinned SHA-256 checksums in .wfctl.yaml before loading. This extends the external plugin lifecycle (install → pin → verify → load) and tightens runtime behavior around plugin supply-chain integrity.

Changes:

  • Introduces .wfctl.yaml-based external plugin binary integrity verification and wires it into external plugin loading.
  • Adds config support for declared external plugins (with optional auto-fetch) and integrates auto-fetch into server startup.
  • Improves plugin scaffolding/tests and adds an HTTP timeout for static registry fetches; enhances workflow_call step behavior.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
plugin/sdk/generator_test.go Adds a project-structure test for plugin scaffolding output.
plugin/integrity.go New integrity verifier for plugin binaries using .wfctl.yaml pinned SHA-256.
plugin/integrity_test.go Unit tests for lockfile discovery and checksum verification behaviors.
plugin/external/manager.go Runs integrity verification before launching external plugin binaries.
plugin/autofetch.go Adds wfctl-shell-out auto-fetch for declared external plugins.
plugin/autofetch_test.go Unit tests for auto-fetch helpers and version constraint handling.
module/pipeline_step_workflow_call.go Adds stop_pipeline support and resolves workflow name via templates.
engine.go Minor formatting-only adjustments.
docs/PLUGIN_AUTHORING.md Updates authoring guidance around plugin naming in plugin.json.
config/config.go Adds plugins.external config shape and merges it during imports.
config/config_test.go Tests parsing for plugins.external and absent plugins section.
cmd/wfctl/registry_source.go Uses a timeout-configured HTTP client for static registry fetches.
cmd/wfctl/plugin_init_test.go Adds comprehensive unit tests for wfctl plugin init scaffold output.
cmd/server/main.go Auto-fetches declared external plugins before discovery/loading.

Comment on lines +12 to +23
// AutoFetchPlugin downloads a plugin from the registry if it's not already installed.
// It shells out to wfctl for the actual download/install logic.
// version is an optional semver constraint (e.g., ">=0.1.0" or "0.2.0").
func AutoFetchPlugin(pluginName, version, pluginDir string) error {
// Check both pluginName and workflow-plugin-<pluginName> (or the short form
// if pluginName already has the "workflow-plugin-" prefix).
if isPluginInstalled(pluginName, pluginDir) {
return nil
}

fmt.Fprintf(os.Stderr, "[auto-fetch] Plugin %q not found locally, fetching from registry...\n", pluginName)

Comment on lines +51 to +55
binaryPath := filepath.Join(pluginDir, pluginName, pluginName)
binaryData, err := os.ReadFile(binaryPath)
if err != nil {
return fmt.Errorf("read plugin binary %s: %w", binaryPath, err)
}
p := filepath.Join(dir, ".wfctl.yaml")
if err := os.WriteFile(p, []byte("plugins:\n my-plugin:\n sha256: abc\n"), 0000); err != nil {
t.Fatalf("write lockfile: %v", err)
}
Comment on lines +109 to +113
workflowName, resolveErr := s.tmpl.Resolve(s.workflow, pc)
if resolveErr != nil {
return nil, fmt.Errorf("workflow_call step %q: failed to resolve workflow name %q: %w", s.name, s.workflow, resolveErr)
}
target, ok := s.lookup(workflowName)
Comment on lines 123 to 133
// WorkflowConfig represents the overall configuration for the workflow engine
type WorkflowConfig struct {
Imports []string `json:"imports,omitempty" yaml:"imports,omitempty"`
Modules []ModuleConfig `json:"modules" yaml:"modules"`
Workflows map[string]any `json:"workflows" yaml:"workflows"`
Triggers map[string]any `json:"triggers" yaml:"triggers"`
Pipelines map[string]any `json:"pipelines,omitempty" yaml:"pipelines,omitempty"`
Platform map[string]any `json:"platform,omitempty" yaml:"platform,omitempty"`
Requires *RequiresConfig `json:"requires,omitempty" yaml:"requires,omitempty"`
Plugins *PluginsConfig `json:"plugins,omitempty" yaml:"plugins,omitempty"`
Sidecars []SidecarConfig `json:"sidecars,omitempty" yaml:"sidecars,omitempty"`
@github-actions
Copy link

github-actions bot commented Mar 15, 2026

⏱ Benchmark Results

No significant performance regressions detected.

benchstat comparison (baseline → PR)
## benchstat: baseline → PR
baseline-bench.txt:244: parsing iteration count: invalid syntax
baseline-bench.txt:293075: parsing iteration count: invalid syntax
baseline-bench.txt:595350: parsing iteration count: invalid syntax
baseline-bench.txt:886630: parsing iteration count: invalid syntax
baseline-bench.txt:1172822: parsing iteration count: invalid syntax
baseline-bench.txt:1468488: parsing iteration count: invalid syntax
benchmark-results.txt:244: parsing iteration count: invalid syntax
benchmark-results.txt:312269: parsing iteration count: invalid syntax
benchmark-results.txt:598830: parsing iteration count: invalid syntax
benchmark-results.txt:935164: parsing iteration count: invalid syntax
benchmark-results.txt:1259882: parsing iteration count: invalid syntax
benchmark-results.txt:1564632: parsing iteration count: invalid syntax
goos: linux
goarch: amd64
pkg: github.com/GoCodeAlone/workflow/dynamic
cpu: AMD EPYC 7763 64-Core Processor                
                            │ baseline-bench.txt │        benchmark-results.txt        │
                            │       sec/op       │    sec/op      vs base              │
InterpreterCreation-4               9.950m ± 69%   10.251m ± 69%       ~ (p=0.589 n=6)
ComponentLoad-4                     3.578m ±  1%    3.644m ±  1%  +1.85% (p=0.002 n=6)
ComponentExecute-4                  2.035µ ±  1%    2.050µ ±  1%  +0.76% (p=0.022 n=6)
PoolContention/workers-1-4          1.119µ ±  2%    1.118µ ±  2%       ~ (p=0.851 n=6)
PoolContention/workers-2-4          1.100µ ±  2%    1.120µ ±  2%  +1.82% (p=0.026 n=6)
PoolContention/workers-4-4          1.129µ ±  3%    1.115µ ±  1%       ~ (p=0.258 n=6)
PoolContention/workers-8-4          1.126µ ±  4%    1.112µ ±  3%       ~ (p=0.084 n=6)
PoolContention/workers-16-4         1.121µ ±  3%    1.115µ ±  0%       ~ (p=0.615 n=6)
ComponentLifecycle-4                3.597m ±  4%    3.726m ±  1%  +3.60% (p=0.026 n=6)
SourceValidation-4                  2.322µ ±  1%    2.348µ ±  1%  +1.14% (p=0.017 n=6)
RegistryConcurrent-4                780.4n ±  3%    819.2n ±  6%       ~ (p=0.132 n=6)
LoaderLoadFromString-4              3.580m ±  1%    3.766m ±  1%  +5.21% (p=0.002 n=6)
geomean                             19.47µ          19.78µ        +1.58%

                            │ baseline-bench.txt │        benchmark-results.txt         │
                            │        B/op        │     B/op      vs base                │
InterpreterCreation-4               1.944Mi ± 0%   1.944Mi ± 0%       ~ (p=0.937 n=6)
ComponentLoad-4                     2.097Mi ± 0%   2.097Mi ± 0%       ~ (p=0.909 n=6)
ComponentExecute-4                  1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4         1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                2.099Mi ± 0%   2.099Mi ± 0%       ~ (p=0.615 n=6)
SourceValidation-4                  1.984Ki ± 0%   1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                1.133Ki ± 0%   1.133Ki ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4              2.099Mi ± 0%   2.099Mi ± 0%       ~ (p=0.457 n=6)
geomean                             15.05Ki        15.05Ki       -0.00%
¹ all samples are equal

                            │ baseline-bench.txt │        benchmark-results.txt        │
                            │     allocs/op      │  allocs/op   vs base                │
InterpreterCreation-4                15.09k ± 0%   15.09k ± 0%       ~ (p=1.000 n=6)
ComponentLoad-4                      17.43k ± 0%   17.43k ± 0%       ~ (p=1.000 n=6)
ComponentExecute-4                    25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4           25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                 17.48k ± 0%   17.48k ± 0%       ~ (p=1.000 n=6) ¹
SourceValidation-4                    32.00 ± 0%    32.00 ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                  2.000 ± 0%    2.000 ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4               17.47k ± 0%   17.47k ± 0%       ~ (p=1.000 n=6) ¹
geomean                               181.2         181.2       +0.00%
¹ all samples are equal

pkg: github.com/GoCodeAlone/workflow/middleware
                                  │ baseline-bench.txt │       benchmark-results.txt       │
                                  │       sec/op       │   sec/op     vs base              │
CircuitBreakerDetection-4                  288.5n ± 4%   291.2n ± 9%       ~ (p=0.095 n=6)
CircuitBreakerExecution_Success-4          22.52n ± 1%   22.50n ± 1%       ~ (p=0.797 n=6)
CircuitBreakerExecution_Failure-4          64.40n ± 0%   65.80n ± 1%  +2.17% (p=0.002 n=6)
geomean                                    74.79n        75.55n       +1.01%

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │        B/op        │    B/op     vs base                │
CircuitBreakerDetection-4                 144.0 ± 0%     144.0 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │     allocs/op      │ allocs/op   vs base                │
CircuitBreakerDetection-4                 1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/module
                                 │ baseline-bench.txt │       benchmark-results.txt        │
                                 │       sec/op       │    sec/op     vs base              │
JQTransform_Simple-4                     910.9n ± 27%   882.1n ± 30%       ~ (p=0.132 n=6)
JQTransform_ObjectConstruction-4         1.515µ ±  1%   1.472µ ±  1%  -2.87% (p=0.002 n=6)
JQTransform_ArraySelect-4                3.587µ ±  1%   3.490µ ±  1%  -2.69% (p=0.002 n=6)
JQTransform_Complex-4                    39.52µ ±  1%   39.22µ ±  0%  -0.76% (p=0.002 n=6)
JQTransform_Throughput-4                 1.857µ ±  1%   1.827µ ±  1%  -1.62% (p=0.009 n=6)
SSEPublishDelivery-4                     65.68n ±  0%   67.54n ±  1%  +2.84% (p=0.002 n=6)
geomean                                  1.697µ         1.673µ        -1.40%

                                 │ baseline-bench.txt │        benchmark-results.txt         │
                                 │        B/op        │     B/op      vs base                │
JQTransform_Simple-4                   1.273Ki ± 0%     1.273Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4       1.773Ki ± 0%     1.773Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4              2.625Ki ± 0%     2.625Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                  16.22Ki ± 0%     16.22Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4               1.984Ki ± 0%     1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²                 +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                 │ baseline-bench.txt │       benchmark-results.txt        │
                                 │     allocs/op      │ allocs/op   vs base                │
JQTransform_Simple-4                     10.00 ± 0%     10.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4         15.00 ± 0%     15.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4                30.00 ± 0%     30.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                    324.0 ± 0%     324.0 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4                 17.00 ± 0%     17.00 ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/schema
                                    │ baseline-bench.txt │       benchmark-results.txt       │
                                    │       sec/op       │   sec/op     vs base              │
SchemaValidation_Simple-4                   1.108µ ± 11%   1.116µ ± 6%       ~ (p=0.563 n=6)
SchemaValidation_AllFields-4                1.676µ ±  2%   1.692µ ± 8%       ~ (p=0.102 n=6)
SchemaValidation_FormatValidation-4         1.603µ ±  3%   1.598µ ± 0%       ~ (p=0.502 n=6)
SchemaValidation_ManySchemas-4              1.826µ ±  1%   1.831µ ± 3%       ~ (p=0.485 n=6)
geomean                                     1.527µ         1.533µ       +0.42%

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │        B/op        │    B/op     vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │     allocs/op      │ allocs/op   vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/store
                                   │ baseline-bench.txt │        benchmark-results.txt        │
                                   │       sec/op       │    sec/op     vs base               │
EventStoreAppend_InMemory-4                1.330µ ± 13%   1.414µ ± 17%        ~ (p=0.310 n=6)
EventStoreAppend_SQLite-4                  1.682m ±  3%   1.291m ±  9%  -23.25% (p=0.002 n=6)
GetTimeline_InMemory/events-10-4           14.22µ ±  4%   14.71µ ±  5%        ~ (p=0.132 n=6)
GetTimeline_InMemory/events-50-4           63.47µ ± 28%   79.29µ ± 22%        ~ (p=0.485 n=6)
GetTimeline_InMemory/events-100-4          127.1µ ±  1%   124.6µ ±  1%   -1.95% (p=0.002 n=6)
GetTimeline_InMemory/events-500-4          646.9µ ±  1%   639.9µ ±  1%   -1.08% (p=0.004 n=6)
GetTimeline_InMemory/events-1000-4         1.323m ±  2%   1.308m ±  2%   -1.16% (p=0.026 n=6)
GetTimeline_SQLite/events-10-4             112.8µ ±  1%   113.5µ ±  1%        ~ (p=0.240 n=6)
GetTimeline_SQLite/events-50-4             260.2µ ±  0%   259.4µ ±  3%        ~ (p=0.589 n=6)
GetTimeline_SQLite/events-100-4            436.6µ ±  1%   436.5µ ±  1%        ~ (p=0.937 n=6)
GetTimeline_SQLite/events-500-4            1.849m ±  1%   1.848m ±  2%        ~ (p=0.937 n=6)
GetTimeline_SQLite/events-1000-4           3.634m ±  3%   3.571m ±  2%   -1.72% (p=0.015 n=6)
geomean                                    227.2µ         227.1µ         -0.04%

                                   │ baseline-bench.txt │        benchmark-results.txt         │
                                   │        B/op        │     B/op      vs base                │
EventStoreAppend_InMemory-4                 778.0 ± 11%     836.5 ± 8%       ~ (p=0.411 n=6)
EventStoreAppend_SQLite-4                 1.984Ki ±  2%   1.982Ki ± 2%       ~ (p=0.496 n=6)
GetTimeline_InMemory/events-10-4          7.953Ki ±  0%   7.953Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4          46.62Ki ±  0%   46.62Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4         94.48Ki ±  0%   94.48Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4         472.8Ki ±  0%   472.8Ki ± 0%       ~ (p=0.545 n=6)
GetTimeline_InMemory/events-1000-4        944.3Ki ±  0%   944.3Ki ± 0%       ~ (p=0.232 n=6)
GetTimeline_SQLite/events-10-4            16.74Ki ±  0%   16.74Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4            87.14Ki ±  0%   87.14Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4           175.4Ki ±  0%   175.4Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4           846.1Ki ±  0%   846.1Ki ± 0%       ~ (p=0.364 n=6)
GetTimeline_SQLite/events-1000-4          1.639Mi ±  0%   1.639Mi ± 0%       ~ (p=1.000 n=6)
geomean                                   67.26Ki         67.66Ki       +0.60%
¹ all samples are equal

                                   │ baseline-bench.txt │        benchmark-results.txt        │
                                   │     allocs/op      │  allocs/op   vs base                │
EventStoreAppend_InMemory-4                  7.000 ± 0%    7.000 ± 0%       ~ (p=1.000 n=6) ¹
EventStoreAppend_SQLite-4                    53.00 ± 0%    53.00 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-10-4             125.0 ± 0%    125.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4             653.0 ± 0%    653.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4           1.306k ± 0%   1.306k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4           6.514k ± 0%   6.514k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-1000-4          13.02k ± 0%   13.02k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-10-4               382.0 ± 0%    382.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4              1.852k ± 0%   1.852k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4             3.681k ± 0%   3.681k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4             18.54k ± 0%   18.54k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-1000-4            37.29k ± 0%   37.29k ± 0%       ~ (p=1.000 n=6) ¹
geomean                                     1.162k        1.162k       +0.00%
¹ all samples are equal

Benchmarks run with go test -bench=. -benchmem -count=6.
Regressions ≥ 20% are flagged. Results compared via benchstat.

intel352 added a commit that referenced this pull request Mar 15, 2026
- registry_source.go: switch GitHubRegistrySource to use registryHTTPClient
  (timeout-configured) for both ListPlugins and FetchManifest; update comment
- autofetch.go: scan for AutoFetch=true entries before exec.LookPath to avoid
  misleading startup warning when no plugins need auto-fetch
- autofetch.go: add internal autoFetchPlugin helper that accepts *slog.Logger
  and emits structured log entries when a logger is available; public
  AutoFetchPlugin delegates to it; AutoFetchDeclaredPlugins passes its logger
- autofetch_test.go: rename TestAutoFetchPlugin_CorrectArgs to
  TestAutoFetchPlugin_SkipsWhenExists to match what the test actually asserts
- integrity.go: replace os.ReadFile + sha256.Sum256 with streaming
  os.Open + io.Copy into sha256.New() to keep memory bounded for large binaries
- integrity_test.go: add t.Skip guard in UnreadableLockfile test when the file
  is actually readable (Windows / root environments)
- pipeline_step_workflow_call.go: use resolved workflowName (not s.workflow)
  in async return payload and sync error message for consistency
- docs/PLUGIN_AUTHORING.md: clarify the distinction between plugin.json name
  (short, used by engine) and the registry/provider manifest name
  (workflow-plugin- prefixed), and which is used for dependency resolution
- config/config.go: MergeApplicationConfig now merges Plugins.External from
  each referenced workflow file into the combined config, deduplicated by name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
intel352 and others added 11 commits March 15, 2026 01:55
Adds --url flag to wfctl plugin install that downloads a tar.gz archive
from a direct URL, extracts plugin.json to identify the plugin name,
installs to the plugin directory, and records the SHA-256 checksum in
the lockfile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extends wfctl plugin init to generate cmd/workflow-plugin-<name>/main.go,
internal/provider.go, internal/steps.go, go.mod, .goreleaser.yml, CI/release
GitHub Actions workflows, Makefile, and README.md. Adds --module flag for
custom Go module paths. Preserves existing plugin.json and .go skeleton for
backward compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add AutoFetchPlugin and AutoFetchDeclaredPlugins to plugin/autofetch.go,
which shell out to wfctl to download plugins not found locally. Extend
WorkflowConfig with a new PluginsConfig / ExternalPluginDecl type so
configs can declare plugins with autoFetch: true and an optional version
constraint. StdEngine gains SetExternalPluginDir and calls
AutoFetchDeclaredPlugins in BuildFromConfig before module loading.
The server's buildEngine registers the plugin dir so auto-fetch is active
at runtime. If wfctl is absent, a warning is logged and startup continues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use struct conversion for staticIndexEntry → PluginSummary (staticcheck S1016)
- Remove unused updateLockfile and writePluginJSON functions
- Add nilerr annotations for intentional nil returns in integrity.go
- Add gosec annotation for exec.Command in autofetch.go
- Fix TestLoadRegistryConfigDefault to use DefaultRegistryConfig() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- integrity.go: fail closed when lockfile exists but is unreadable or
  unparseable, preventing integrity enforcement bypass
- autofetch.go: extract stripVersionConstraint helper; detect compound
  version constraints and fall back to latest; check both pluginName and
  workflow-plugin-<name> alternate form for installed-check; log restart
  warning when new plugins are downloaded (they require a server restart)
- autofetch_test.go: test stripVersionConstraint directly instead of
  duplicating the logic inline; add compound-constraint cases
- engine.go: clarify comment that auto-fetched plugins need a restart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- generator.go: use plugin/external/sdk imports and types (PluginProvider,
  StepInstance, StepResult, StepTypes/CreateStep) instead of plugin/sdk
- PLUGIN_AUTHORING.md: update examples to match external SDK interfaces
- plugin_install.go: hash installed binary (not archive) for lockfile,
  add hashFileSHA256 helper, add install mode mutual exclusivity check,
  update installFromLocal to write lockfile, normalize plugin names
- plugin_lockfile.go: add registry param to updateLockfileWithChecksum,
  pass version/registry in installFromLockfile, remove dir on mismatch
- registry_source.go: validate URL in NewStaticRegistrySource
- config.go: clarify Version field forwarding semantics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- registry_source.go: use explicit field assignment for PluginSummary
  instead of struct type conversion (clearer, avoids tag confusion)
- plugin_lockfile.go: don't pass @Version in installFromLockfile to
  prevent lockfile overwrite before checksum verification
- plugin_install.go: add verifyInstalledPlugin() call in installFromURL
  for parity with registry installs
- engine.go: add TODO to move auto-fetch before plugin discovery so
  newly fetched plugins are available without restart
- integrity_test.go: add tests for unreadable and malformed lockfile
  to verify fail-closed behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- registry_source.go: add nolint:gosimple for S1016 — explicit field
  assignment preferred for clarity across different struct tags
- generator_test.go: add TestGenerateProjectStructure verifying all
  generated files exist and use correct external SDK imports/types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…immediately

Auto-fetch was running inside BuildFromConfig, which executes after
external plugins are already discovered and loaded. Plugins downloaded
by auto-fetch required a server restart to take effect.

Move auto-fetch to buildEngine in cmd/server/main.go, before
DiscoverPlugins/LoadPlugin. Remove the now-unused externalPluginDir
field and SetExternalPluginDir from the engine.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enables pipelines to call other pipelines by name with full context
forwarding. Supports template-resolved workflow names and stop_pipeline
option. Required for WebSocket message routing to game-specific pipelines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- registry_source.go: switch GitHubRegistrySource to use registryHTTPClient
  (timeout-configured) for both ListPlugins and FetchManifest; update comment
- autofetch.go: scan for AutoFetch=true entries before exec.LookPath to avoid
  misleading startup warning when no plugins need auto-fetch
- autofetch.go: add internal autoFetchPlugin helper that accepts *slog.Logger
  and emits structured log entries when a logger is available; public
  AutoFetchPlugin delegates to it; AutoFetchDeclaredPlugins passes its logger
- autofetch_test.go: rename TestAutoFetchPlugin_CorrectArgs to
  TestAutoFetchPlugin_SkipsWhenExists to match what the test actually asserts
- integrity.go: replace os.ReadFile + sha256.Sum256 with streaming
  os.Open + io.Copy into sha256.New() to keep memory bounded for large binaries
- integrity_test.go: add t.Skip guard in UnreadableLockfile test when the file
  is actually readable (Windows / root environments)
- pipeline_step_workflow_call.go: use resolved workflowName (not s.workflow)
  in async return payload and sync error message for consistency
- docs/PLUGIN_AUTHORING.md: clarify the distinction between plugin.json name
  (short, used by engine) and the registry/provider manifest name
  (workflow-plugin- prefixed), and which is used for dependency resolution
- config/config.go: MergeApplicationConfig now merges Plugins.External from
  each referenced workflow file into the combined config, deduplicated by name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 15, 2026 06:03
@intel352 intel352 force-pushed the feat/plugin-ecosystem-engine branch from 9dccf19 to 56469b1 Compare March 15, 2026 06:03
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves plugin management (integrity verification, registry fetching, install/lockfile behavior, and auto-fetch logging), extends pipeline behavior for workflow_call, and updates authoring documentation to clarify plugin naming.

Changes:

  • Stream SHA-256 hashing for plugin integrity checks; improve tests around lockfile unreadability.
  • Add structured logging option to plugin auto-fetch and reduce unnecessary wfctl checks; centralize registry HTTP client usage with timeouts.
  • Enhance workflow_call pipeline step (templated workflow name + optional stop_pipeline), and merge plugins.external across application workflow files.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
plugin/integrity.go Switches integrity hashing to streaming (io.Copy) to avoid loading binaries into memory.
plugin/integrity_test.go Makes unreadable-lockfile test resilient across platforms/privileged execution.
plugin/autofetch.go Adds internal logger-aware implementation and avoids wfctl lookup when not needed.
plugin/autofetch_test.go Renames/adjusts an auto-fetch test (now overlaps with an existing one).
module/pipeline_step_workflow_call.go Resolves workflow name via templates and supports stopping parent pipeline after call.
docs/PLUGIN_AUTHORING.md Clarifies plugin.json naming expectations and short vs prefixed names.
config/config.go Merges/deduplicates plugins.external when merging application workflow files.
cmd/wfctl/registry_source.go Uses a shared HTTP client with timeout for registry operations; avoids default client.
cmd/wfctl/plugin_lockfile.go Improves behavior on checksum mismatch and reports removal failures.
cmd/wfctl/plugin_install.go Tweaks mode validation messaging, lockfile checksum handling, and adds helper hashing function.
cmd/wfctl/multi_registry.go Minor rename/cleanup while building registry sources.
Comments suppressed due to low confidence (1)

plugin/autofetch_test.go:103

  • This test now duplicates TestAutoFetchPlugin_AlreadyInstalled (same behavior: returns nil when plugin.json exists). Keeping both adds maintenance cost without increasing coverage. Consider removing one of them or rewriting this one to validate a distinct behavior (e.g., the logger-aware autoFetchPlugin path / version constraint handling).
// TestAutoFetchPlugin_SkipsWhenExists verifies that AutoFetchPlugin returns nil
// immediately when the plugin is already installed, without invoking wfctl.
func TestAutoFetchPlugin_SkipsWhenExists(t *testing.T) {
	dir := t.TempDir()
	pluginDir := filepath.Join(dir, "plugins")
	pluginName := "test-plugin"

	destDir := filepath.Join(pluginDir, pluginName)
	if err := os.MkdirAll(destDir, 0755); err != nil {
		t.Fatalf("failed to create plugin dir: %v", err)
	}
	manifestPath := filepath.Join(destDir, "plugin.json")
	if err := os.WriteFile(manifestPath, []byte(`{"name":"test-plugin","version":"0.1.0"}`), 0644); err != nil {
		t.Fatalf("failed to write plugin.json: %v", err)
	}

	// With plugin.json present, AutoFetchPlugin must return nil (no wfctl invoked).
	if err := AutoFetchPlugin(pluginName, ">=0.1.0", pluginDir); err != nil {
		t.Errorf("expected nil for already-installed plugin, got: %v", err)
	}

Comment on lines 76 to 86
// CreateStep implements sdk.StepProvider.
func (p *Provider) CreateStep(typeName, name string, config map[string]any) (sdk.StepInstance, error) {
switch typeName {
case "step.my_action":
return &MyStep{config: config}, nil
}
return nil, fmt.Errorf("unknown step type: %s", typeName)
}
```

## Implementing Modules

Comment on lines +423 to +439
// Merge external plugin declarations — deduplicate by name (first definition wins).
if wfCfg.Plugins != nil && len(wfCfg.Plugins.External) > 0 {
if combined.Plugins == nil {
combined.Plugins = &PluginsConfig{}
}
existingPlugins := make(map[string]struct{}, len(combined.Plugins.External))
for _, ep := range combined.Plugins.External {
existingPlugins[ep.Name] = struct{}{}
}
for _, ep := range wfCfg.Plugins.External {
if _, exists := existingPlugins[ep.Name]; exists {
continue
}
combined.Plugins.External = append(combined.Plugins.External, ep)
existingPlugins[ep.Name] = struct{}{}
}
}
Comment on lines 167 to 175
if _, ver := parseNameVersion(nameArg); ver != "" {
// Hash the installed binary (not the archive) so verifyInstalledChecksum matches.
pluginName = normalizePluginName(pluginName)
binaryChecksum := ""
binaryPath := filepath.Join(pluginDirVal, pluginName, pluginName)
sha, hashErr := hashFileSHA256(binaryPath)
if hashErr != nil {
fmt.Fprintf(os.Stderr, "warning: could not hash installed binary: %v\n", hashErr)
if cs, hashErr := hashFileSHA256(binaryPath); hashErr == nil {
binaryChecksum = cs
}
updateLockfileWithChecksum(pluginName, manifest.Version, manifest.Repository, sourceName, sha)
updateLockfileWithChecksum(pluginName, manifest.Version, manifest.Repository, sourceName, binaryChecksum)
}
Comment on lines +539 to +547
// hashFileSHA256 computes the SHA-256 hex digest of the file at path.
func hashFileSHA256(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("hash file %s: %w", path, err)
}
h := sha256.Sum256(data)
return hex.EncodeToString(h[:]), nil
}
intel352 and others added 2 commits March 15, 2026 02:07
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- docs/PLUGIN_AUTHORING.md: close unclosed code fence after StepProvider example
- cmd/wfctl/plugin_install.go: streaming hashFileSHA256 via io.Copy + sha256.New()
- cmd/wfctl/plugin_install.go: warn on hash failure instead of silent empty checksum
- config/merge_test.go: add TestMergeApplicationConfig_PluginDedup covering first-definition-wins dedup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 15, 2026 06:13
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR tightens and standardizes plugin-related behavior across the engine and wfctl by improving checksum handling, registry HTTP behavior, and application config merging, while adding a new pipeline control option for workflow_call steps.

Changes:

  • Switch plugin/binary SHA-256 computation to streaming I/O and harden related tests for cross-platform behavior.
  • Add structured logging support to plugin auto-fetch and avoid unnecessary wfctl checks when auto-fetch isn’t requested.
  • Merge/deduplicate external plugin declarations across application workflow files and extend workflow_call with templated workflow names + stop_pipeline.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
plugin/integrity.go Stream SHA-256 hashing for plugin binary integrity verification.
plugin/integrity_test.go Make unreadable-lockfile test robust on platforms where chmod isn’t enforced.
plugin/autofetch.go Add internal logger-capable implementation; avoid wfctl lookup when no auto-fetch is requested.
plugin/autofetch_test.go Adjust/rename test around auto-fetch short-circuit behavior.
module/pipeline_step_workflow_call.go Add stop_pipeline and template-based workflow name resolution for workflow_call.
docs/PLUGIN_AUTHORING.md Clarify plugin naming expectations between plugin.json and registry naming.
config/config.go Deduplicate merged external plugin declarations by name in MergeApplicationConfig.
config/merge_test.go Add coverage for plugin deduplication in application config merging.
cmd/wfctl/registry_source.go Route registry requests through a shared HTTP client with timeout; minor search result construction tweak.
cmd/wfctl/plugin_lockfile.go Improve checksum-mismatch handling by warning on removal failures.
cmd/wfctl/plugin_install.go Improve mode validation messaging; compute lockfile checksums via streaming hash helper.
cmd/wfctl/multi_registry.go Minor variable naming cleanup in static registry creation.
Comments suppressed due to low confidence (1)

plugin/autofetch.go:155

  • When wfctl is missing and logger is nil, this returns silently even if there are AutoFetch=true declarations. That contradicts the function comment (“a warning is logged”) and can make auto-fetch failures hard to diagnose in non-structured-logging callers. Consider also printing a stderr warning when logger is nil (or return an error/bool so the caller can surface it).
	if _, err := exec.LookPath("wfctl"); err != nil {
		if logger != nil {
			logger.Warn("wfctl not found on PATH; skipping auto-fetch for declared plugins",
				"plugin_dir", pluginDir)
		}

_, _ = target.Execute(asyncCtx, data) //nolint:errcheck
}(ctx, triggerData)
return &StepResult{Output: map[string]any{"workflow": s.workflow, "mode": "async", "dispatched": true}}, nil
return &StepResult{Output: map[string]any{"workflow": workflowName, "mode": "async", "dispatched": true}}, nil
binaryData, err := os.ReadFile(binaryPath)
f, err := os.Open(binaryPath)
if err != nil {
return fmt.Errorf("read plugin binary %s: %w", binaryPath, err)
if cs, hashErr := hashFileSHA256(binaryPath); hashErr == nil {
binaryChecksum = cs
} else {
fmt.Fprintf(os.Stderr, "Warning: could not hash binary %s: %v (lockfile will have no checksum)\n", binaryPath, hashErr)
Comment on lines +541 to +543
wfA := dir + "/a.yaml"
writeFileContent(wfA, `
plugins:
- pipeline_step_workflow_call.go: propagate stop_pipeline in async mode
- integrity.go: "open plugin binary" instead of "read" for os.Open error
- plugin_install.go: lowercase "warning:" for consistency
- merge_test.go: use filepath.Join and check writeFileContent errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@intel352 intel352 merged commit 76ec00d into main Mar 15, 2026
15 checks passed
@intel352 intel352 deleted the feat/plugin-ecosystem-engine branch March 15, 2026 06:32
intel352 added a commit that referenced this pull request Mar 16, 2026
* feat(wfctl): add plugin install --url for direct URL installs

Adds --url flag to wfctl plugin install that downloads a tar.gz archive
from a direct URL, extracts plugin.json to identify the plugin name,
installs to the plugin directory, and records the SHA-256 checksum in
the lockfile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(wfctl): enhanced plugin init scaffold with full project structure

Extends wfctl plugin init to generate cmd/workflow-plugin-<name>/main.go,
internal/provider.go, internal/steps.go, go.mod, .goreleaser.yml, CI/release
GitHub Actions workflows, Makefile, and README.md. Adds --module flag for
custom Go module paths. Preserves existing plugin.json and .go skeleton for
backward compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: engine auto-fetch for declared external plugins on startup

Add AutoFetchPlugin and AutoFetchDeclaredPlugins to plugin/autofetch.go,
which shell out to wfctl to download plugins not found locally. Extend
WorkflowConfig with a new PluginsConfig / ExternalPluginDecl type so
configs can declare plugins with autoFetch: true and an optional version
constraint. StdEngine gains SetExternalPluginDir and calls
AutoFetchDeclaredPlugins in BuildFromConfig before module loading.
The server's buildEngine registers the plugin dir so auto-fetch is active
at runtime. If wfctl is absent, a warning is logged and startup continues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: resolve all CI lint failures

- Use struct conversion for staticIndexEntry → PluginSummary (staticcheck S1016)
- Remove unused updateLockfile and writePluginJSON functions
- Add nilerr annotations for intentional nil returns in integrity.go
- Add gosec annotation for exec.Command in autofetch.go
- Fix TestLoadRegistryConfigDefault to use DefaultRegistryConfig() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address Copilot review feedback on engine PR

- integrity.go: fail closed when lockfile exists but is unreadable or
  unparseable, preventing integrity enforcement bypass
- autofetch.go: extract stripVersionConstraint helper; detect compound
  version constraints and fall back to latest; check both pluginName and
  workflow-plugin-<name> alternate form for installed-check; log restart
  warning when new plugins are downloaded (they require a server restart)
- autofetch_test.go: test stripVersionConstraint directly instead of
  duplicating the logic inline; add compound-constraint cases
- engine.go: clarify comment that auto-fetched plugins need a restart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address all Copilot review comments on engine PR (#330)

- generator.go: use plugin/external/sdk imports and types (PluginProvider,
  StepInstance, StepResult, StepTypes/CreateStep) instead of plugin/sdk
- PLUGIN_AUTHORING.md: update examples to match external SDK interfaces
- plugin_install.go: hash installed binary (not archive) for lockfile,
  add hashFileSHA256 helper, add install mode mutual exclusivity check,
  update installFromLocal to write lockfile, normalize plugin names
- plugin_lockfile.go: add registry param to updateLockfileWithChecksum,
  pass version/registry in installFromLockfile, remove dir on mismatch
- registry_source.go: validate URL in NewStaticRegistrySource
- config.go: clarify Version field forwarding semantics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address remaining Copilot review comments on engine PR (#330)

- registry_source.go: use explicit field assignment for PluginSummary
  instead of struct type conversion (clearer, avoids tag confusion)
- plugin_lockfile.go: don't pass @Version in installFromLockfile to
  prevent lockfile overwrite before checksum verification
- plugin_install.go: add verifyInstalledPlugin() call in installFromURL
  for parity with registry installs
- engine.go: add TODO to move auto-fetch before plugin discovery so
  newly fetched plugins are available without restart
- integrity_test.go: add tests for unreadable and malformed lockfile
  to verify fail-closed behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress S1016 lint, add generator project structure tests

- registry_source.go: add nolint:gosimple for S1016 — explicit field
  assignment preferred for clarity across different struct tags
- generator_test.go: add TestGenerateProjectStructure verifying all
  generated files exist and use correct external SDK imports/types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: move auto-fetch before plugin discovery so fetched plugins load immediately

Auto-fetch was running inside BuildFromConfig, which executes after
external plugins are already discovered and loaded. Plugins downloaded
by auto-fetch required a server restart to take effect.

Move auto-fetch to buildEngine in cmd/server/main.go, before
DiscoverPlugins/LoadPlugin. Remove the now-unused externalPluginDir
field and SetExternalPluginDir from the engine.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add step.workflow_call for cross-pipeline dispatch

Enables pipelines to call other pipelines by name with full context
forwarding. Supports template-resolved workflow names and stop_pipeline
option. Required for WebSocket message routing to game-specific pipelines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address all 9 Copilot review comments on PR #331

- registry_source.go: switch GitHubRegistrySource to use registryHTTPClient
  (timeout-configured) for both ListPlugins and FetchManifest; update comment
- autofetch.go: scan for AutoFetch=true entries before exec.LookPath to avoid
  misleading startup warning when no plugins need auto-fetch
- autofetch.go: add internal autoFetchPlugin helper that accepts *slog.Logger
  and emits structured log entries when a logger is available; public
  AutoFetchPlugin delegates to it; AutoFetchDeclaredPlugins passes its logger
- autofetch_test.go: rename TestAutoFetchPlugin_CorrectArgs to
  TestAutoFetchPlugin_SkipsWhenExists to match what the test actually asserts
- integrity.go: replace os.ReadFile + sha256.Sum256 with streaming
  os.Open + io.Copy into sha256.New() to keep memory bounded for large binaries
- integrity_test.go: add t.Skip guard in UnreadableLockfile test when the file
  is actually readable (Windows / root environments)
- pipeline_step_workflow_call.go: use resolved workflowName (not s.workflow)
  in async return payload and sync error message for consistency
- docs/PLUGIN_AUTHORING.md: clarify the distinction between plugin.json name
  (short, used by engine) and the registry/provider manifest name
  (workflow-plugin- prefixed), and which is used for dependency resolution
- config/config.go: MergeApplicationConfig now merges Plugins.External from
  each referenced workflow file into the combined config, deduplicated by name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove duplicate hashFileSHA256 from merge conflict resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address new PR #331 review comments

- docs/PLUGIN_AUTHORING.md: close unclosed code fence after StepProvider example
- cmd/wfctl/plugin_install.go: streaming hashFileSHA256 via io.Copy + sha256.New()
- cmd/wfctl/plugin_install.go: warn on hash failure instead of silent empty checksum
- config/merge_test.go: add TestMergeApplicationConfig_PluginDedup covering first-definition-wins dedup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address 4 new PR #331 review comments

- pipeline_step_workflow_call.go: propagate stop_pipeline in async mode
- integrity.go: "open plugin binary" instead of "read" for os.Open error
- plugin_install.go: lowercase "warning:" for consistency
- merge_test.go: use filepath.Join and check writeFileContent errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add skip_if and if fields to pipeline step execution

Steps now support optional `skip_if` and `if` Go template guards.
When `skip_if` evaluates to a truthy value the step is skipped and
the pipeline continues; `if` is the logical inverse. Skipped steps
produce `{"skipped": true, "reason": "..."}` output so downstream
steps can inspect the result.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
intel352 added a commit that referenced this pull request Mar 16, 2026
…adata keys for skipped output (#333)

* feat(wfctl): add plugin install --url for direct URL installs

Adds --url flag to wfctl plugin install that downloads a tar.gz archive
from a direct URL, extracts plugin.json to identify the plugin name,
installs to the plugin directory, and records the SHA-256 checksum in
the lockfile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(wfctl): enhanced plugin init scaffold with full project structure

Extends wfctl plugin init to generate cmd/workflow-plugin-<name>/main.go,
internal/provider.go, internal/steps.go, go.mod, .goreleaser.yml, CI/release
GitHub Actions workflows, Makefile, and README.md. Adds --module flag for
custom Go module paths. Preserves existing plugin.json and .go skeleton for
backward compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: engine auto-fetch for declared external plugins on startup

Add AutoFetchPlugin and AutoFetchDeclaredPlugins to plugin/autofetch.go,
which shell out to wfctl to download plugins not found locally. Extend
WorkflowConfig with a new PluginsConfig / ExternalPluginDecl type so
configs can declare plugins with autoFetch: true and an optional version
constraint. StdEngine gains SetExternalPluginDir and calls
AutoFetchDeclaredPlugins in BuildFromConfig before module loading.
The server's buildEngine registers the plugin dir so auto-fetch is active
at runtime. If wfctl is absent, a warning is logged and startup continues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: resolve all CI lint failures

- Use struct conversion for staticIndexEntry → PluginSummary (staticcheck S1016)
- Remove unused updateLockfile and writePluginJSON functions
- Add nilerr annotations for intentional nil returns in integrity.go
- Add gosec annotation for exec.Command in autofetch.go
- Fix TestLoadRegistryConfigDefault to use DefaultRegistryConfig() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address Copilot review feedback on engine PR

- integrity.go: fail closed when lockfile exists but is unreadable or
  unparseable, preventing integrity enforcement bypass
- autofetch.go: extract stripVersionConstraint helper; detect compound
  version constraints and fall back to latest; check both pluginName and
  workflow-plugin-<name> alternate form for installed-check; log restart
  warning when new plugins are downloaded (they require a server restart)
- autofetch_test.go: test stripVersionConstraint directly instead of
  duplicating the logic inline; add compound-constraint cases
- engine.go: clarify comment that auto-fetched plugins need a restart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address all Copilot review comments on engine PR (#330)

- generator.go: use plugin/external/sdk imports and types (PluginProvider,
  StepInstance, StepResult, StepTypes/CreateStep) instead of plugin/sdk
- PLUGIN_AUTHORING.md: update examples to match external SDK interfaces
- plugin_install.go: hash installed binary (not archive) for lockfile,
  add hashFileSHA256 helper, add install mode mutual exclusivity check,
  update installFromLocal to write lockfile, normalize plugin names
- plugin_lockfile.go: add registry param to updateLockfileWithChecksum,
  pass version/registry in installFromLockfile, remove dir on mismatch
- registry_source.go: validate URL in NewStaticRegistrySource
- config.go: clarify Version field forwarding semantics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address remaining Copilot review comments on engine PR (#330)

- registry_source.go: use explicit field assignment for PluginSummary
  instead of struct type conversion (clearer, avoids tag confusion)
- plugin_lockfile.go: don't pass @Version in installFromLockfile to
  prevent lockfile overwrite before checksum verification
- plugin_install.go: add verifyInstalledPlugin() call in installFromURL
  for parity with registry installs
- engine.go: add TODO to move auto-fetch before plugin discovery so
  newly fetched plugins are available without restart
- integrity_test.go: add tests for unreadable and malformed lockfile
  to verify fail-closed behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress S1016 lint, add generator project structure tests

- registry_source.go: add nolint:gosimple for S1016 — explicit field
  assignment preferred for clarity across different struct tags
- generator_test.go: add TestGenerateProjectStructure verifying all
  generated files exist and use correct external SDK imports/types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: move auto-fetch before plugin discovery so fetched plugins load immediately

Auto-fetch was running inside BuildFromConfig, which executes after
external plugins are already discovered and loaded. Plugins downloaded
by auto-fetch required a server restart to take effect.

Move auto-fetch to buildEngine in cmd/server/main.go, before
DiscoverPlugins/LoadPlugin. Remove the now-unused externalPluginDir
field and SetExternalPluginDir from the engine.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add step.workflow_call for cross-pipeline dispatch

Enables pipelines to call other pipelines by name with full context
forwarding. Supports template-resolved workflow names and stop_pipeline
option. Required for WebSocket message routing to game-specific pipelines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address all 9 Copilot review comments on PR #331

- registry_source.go: switch GitHubRegistrySource to use registryHTTPClient
  (timeout-configured) for both ListPlugins and FetchManifest; update comment
- autofetch.go: scan for AutoFetch=true entries before exec.LookPath to avoid
  misleading startup warning when no plugins need auto-fetch
- autofetch.go: add internal autoFetchPlugin helper that accepts *slog.Logger
  and emits structured log entries when a logger is available; public
  AutoFetchPlugin delegates to it; AutoFetchDeclaredPlugins passes its logger
- autofetch_test.go: rename TestAutoFetchPlugin_CorrectArgs to
  TestAutoFetchPlugin_SkipsWhenExists to match what the test actually asserts
- integrity.go: replace os.ReadFile + sha256.Sum256 with streaming
  os.Open + io.Copy into sha256.New() to keep memory bounded for large binaries
- integrity_test.go: add t.Skip guard in UnreadableLockfile test when the file
  is actually readable (Windows / root environments)
- pipeline_step_workflow_call.go: use resolved workflowName (not s.workflow)
  in async return payload and sync error message for consistency
- docs/PLUGIN_AUTHORING.md: clarify the distinction between plugin.json name
  (short, used by engine) and the registry/provider manifest name
  (workflow-plugin- prefixed), and which is used for dependency resolution
- config/config.go: MergeApplicationConfig now merges Plugins.External from
  each referenced workflow file into the combined config, deduplicated by name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove duplicate hashFileSHA256 from merge conflict resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address new PR #331 review comments

- docs/PLUGIN_AUTHORING.md: close unclosed code fence after StepProvider example
- cmd/wfctl/plugin_install.go: streaming hashFileSHA256 via io.Copy + sha256.New()
- cmd/wfctl/plugin_install.go: warn on hash failure instead of silent empty checksum
- config/merge_test.go: add TestMergeApplicationConfig_PluginDedup covering first-definition-wins dedup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address 4 new PR #331 review comments

- pipeline_step_workflow_call.go: propagate stop_pipeline in async mode
- integrity.go: "open plugin binary" instead of "read" for os.Open error
- plugin_install.go: lowercase "warning:" for consistency
- merge_test.go: use filepath.Join and check writeFileContent errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add skip_if and if fields to pipeline step execution

Steps now support optional `skip_if` and `if` Go template guards.
When `skip_if` evaluates to a truthy value the step is skipped and
the pipeline continues; `if` is the logical inverse. Skipped steps
produce `{"skipped": true, "reason": "..."}` output so downstream
steps can inspect the result.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Initial plan

* fix: address review feedback on SkippableStep guard template handling

- Return errors (fail closed) instead of swallowing skip_if/if template failures
- Use _skipped/_error keys in skipped step output (align with ErrorStrategySkip convention)
- Fix misleading comment for If field in config/pipeline.go
- Update tests to reflect new key names; add 2 tests for template error behavior

Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>

---------

Co-authored-by: Jon Langevin <codingsloth@pm.me>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
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