Workflow extension points — how should downstream add deterministic steps without overriding installed YAML? #3473
markuswondrak
started this conversation in
General
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
I am working on a end-to-end agent development flow: Extended Flow, that ships three workflows (feature, bugfix, quick) that downstream projects install via specify bundle install incuding preset and extension.
A problem I am currently facing: a downstream project (a project that uses extended-flow) that needs a deterministic step (e.g. lint, type-check, security scan) cannot add it without overriding the upstream workflow YAML, which breaks upgradeability.
I see two approches for downstream projects to implement this, but both have short-comings:
after_implement,before_specify, etc.) — registered in.specify/extensions.yml, dispatched by the agent reading the core command template..specify/workflows/<workflow-id>/workflow.ymldirectly.Why hooks are not the answer
Spec-Kit's
HookExecutorpersists hook metadata at install time, but has no runtime callers.check_hooks_for_event()andexecute_hook()are documented as "designed to be called by AI agents" — the actual execution is delegated to the agent. The workflow engine (engine.py,CommandStep.execute) has zero hook awareness.Additionally, hook instructions are re-injected into the prompt on every command invocation inside the QA do-while loop. #2943 already documents template bloat; repeated hook checks amplify the token waste.
(I talked about this in my article in April)
Why workflow overrides are not the answer
.specify/workflows/<id>/workflow.ymlis installed config. The nextspecify bundle installorspecify bundle updatewill replace it. A downstream project that edits the workflow locally must re-merge on every Extended Flow release. This is documented maintenance debt with no migration tooling.The actual problem
The root cause is not in Extended Flow's workflow design. It is in Spec-Kit's workflow architecture: there is no mechanism for an upstream workflow to declare an optional, named extension point that a downstream project can fill without touching the workflow file itself.
Possible solutions
The design space has two axes: who declares the extension point (upstream opts in vs. downstream imposes) and how the extension is resolved (manifest, file convention, inheritance, engine-level execution).
1. Step override
Downstream provides a step definition that replaces or augments a named step in the upstream workflow. The engine merges at load time.
Implementation:
.specify/overrides/<workflow-id>/<step-name>.ymlcontains the replacement step definition. The workflow engine resolves overrides after loading the base workflow YAML.Trade-offs:
2. Workflow inheritance
Downstream creates a workflow that extends the upstream one, adding steps at named anchors (before/after/replace).
Implementation:
.specify/workflows/<id>/workflow.ymldeclaresextends: extended-flow/featureand usesinsert_after: implementorreplace: verify-specto position new steps. The engine resolves the base workflow and merges the step list.Trade-offs:
3. Runtime-executed hooks (pre/post-phase)
Spec-Kit adds actual runtime hook execution at well-defined lifecycle points — not agent-dispatched, but engine-executed.
Implementation: The workflow engine calls
HookExecutor.execute_hook()before and after each phase. Hooks are shell commands defined in.specify/extensions.ymlwithauto_run: true. The engine, not the agent, is responsible for execution.Trade-offs:
4. Plugin step (named extension point)
Upstream workflow explicitly declares a named slot at a specific position. Downstream fills it via a separate manifest.
Implementation: Upstream workflow contains
{ type: plugin, name: lint }. Engine looks up.specify/plugins/lint.ymlfor the concrete step definition (type, command, conditions). If no plugin is registered, the step is a no-op.Trade-offs:
5. Workflow fragments (convention-based includes)
Upstream workflow declares an include point. Engine loads all matching fragment files from a well-known directory.
Implementation: Upstream workflow contains
{ include: post-implement }. Engine loads all.specify/fragments/post-implement/*.ymlfiles and inserts them at that position. No explicit plugin registration needed.Trade-offs:
do_action('post_implement'))I am looking for the project's intended direction on this. The concrete pain point is a downstream project that uses Extended Flow and needs to add linting to its development workflow, but the architectural gap is upstream: how can a preset offer deterministic, upgrade-safe extension points when Spec-Kit workflows are static YAML with no templating or inheritance?
Beta Was this translation helpful? Give feedback.
All reactions