## Summary The run Overview tab's "Created by" cell rendered every user as a colored circle with the first letter of their login. Reviewers and run owners expected the same GitHub avatar shown on `/profile` and in the top-right nav. The cell only had `login` to work with — the `PrincipalUser` schema carried no avatar URL. This threads an optional `avatar_url` through `UserPrincipal` end-to-end: schema, server auth, and frontend. The avatar is captured at action time from the request's auth context and persisted with the run's `created_by` principal — a point-in-time snapshot, the same pattern as audit logs and chat apps. ## What changed - **`fabro-types`** — `UserPrincipal` gains `avatar_url: Option<String>` with `#[serde(default, skip_serializing_if)]`, plus a `Principal::user_with_avatar` constructor. The existing `Principal::user` constructor is unchanged (sets `None`), so test fixtures and CLI/replay call sites need no edits. - **OpenAPI** — `PrincipalUser` gains an optional nullable `avatar_url`; Rust (progenitor) and TypeScript clients regenerated. - **`fabro-server`** — `auth_context_from_session` (cookie auth) and `classify_user_token` (JWT auth) populate the principal's avatar from the session/JWT, treating an empty string as `None`. - **`fabro-web`** — the `run-summary-panel` "Created by" cell renders an `<img>` when `avatar_url` is present, falling back to the initial circle otherwise. ## Compatibility The field is optional with serde defaults, so old persisted runs and `RunEvent.actor` payloads deserialize unchanged — they show the initial-circle fallback. No migration or backfill. ## Known gap CLI-initiated runs (`fabro run ...`) still show the initial circle: the CLI auth flow hardcodes an empty `avatar_url` in the JWT subject (`cli_flow.rs:508`). Wiring the avatar through CLI login (`~/.fabro/auth.json`, JWT claims, refresh-token chain) is a deliberate follow-up. Web-initiated runs get the avatar today. ## Test plan - `cargo nextest run --workspace` — 5,832 tests pass, including new `principal.rs` and `principal_round_trip.rs` cases covering avatar serialization and legacy-JSON (no-field) deserialization. - `cd apps/fabro-web && bun test run-summary-panel` — 13 tests pass, including a new case asserting the `<img>` renders with the avatar src. - `bun run typecheck`, `cargo +nightly-2026-04-14 fmt --check --all`, and `clippy --workspace --all-targets -- -D warnings` all clean. - Manual: restart `fabro server`, create a run from the web UI, confirm the real avatar renders on the Overview tab; confirm an older run falls back to the initial circle. --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.6 KiB
Static Workflow Structure and Single Attribute Rendering Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Make workflow structure static and render workflow templates exactly once before validation/execution.
Architecture: Fabro parses raw Graphviz first, resolves only literal workflow/file references, then renders final string attributes with one workflow-definition renderer. Undefined runtime inputs are collected as diagnostics; validate leaves them as warnings, while run-style callers promote them to errors before proceeding.
Tech Stack: Rust, fabro-workflow, fabro-manifest, Graphviz parser, MiniJinja via fabro-template, cargo nextest.
Summary
Remove source-level templating from root workflows, imported .fabro files, manifest scanning, and runtime prompt handlers. Templates remain supported inside final string attribute content such as goal, prompt, script, and label; templates are not supported in graph syntax, node IDs, edges, import paths, @file paths, child workflow paths, or other file references.
Rendering is always lenient for undefined variables and always emits template_undefined_variable diagnostics. Strictness becomes a consumer policy: validation keeps those diagnostics as warnings, and run/create/preflight promotes that diagnostic rule to errors.
Pre-Task Audit
- Run and save an audit summary in the PR description:
rg -n "\\{\\{|\\{%|\\{#" . -g '!target/**' -g '!**/node_modules/**' -g '!**/dist/**' -g '!tmp/**'rg -n "fabro_template::|render_template\\(|render_lenient\\(|render_scan_template|TemplateContext" lib/crates -g '*.rs'
- Classify findings before implementation:
- workflow-definition templates: in scope
- config/env interpolation (
{{ env.* }}in config, hooks, server settings): out of scope - release/web/template assets: out of scope
- docs/examples describing dynamic workflow structure or strict prompt undefined behavior: update
- Known current in-scope breakpoints from the audit:
lib/crates/fabro-manifest/src/lib.rstests currently assert input-driven@file,import, child workflow, and graph-goal file paths.docs/public/workflows/imports.mdx,docs/public/workflows/variables.mdx, anddocs/public/execution/run-configuration.mdxdocument source-level or strict workflow templating semantics.lib/crates/fabro-workflow/src/handler/agent.rsperforms runtime prompt rendering and must stop owning workflow-definition interpolation.
Key Changes
- Remove
RenderModefrom workflow-definition rendering:- delete it from
ValidateInput,TransformOptions,manifest_validation::validate_manifest, and related CLI/server call sites - keep rendering behavior mode-free inside the transform pipeline
- add
Validated::promote_rule_to_error(rule: &str)and callvalidated.promote_rule_to_error(TEMPLATE_UNDEFINED_VARIABLE_RULE)from run/create/preflight/manager-loop paths beforehas_errors()orraise_on_errors()
- delete it from
- Make
TemplateTransformthe single workflow-definition renderer:- render final graph/node/edge string attributes after imports and file inlining
- call MiniJinja leniently for undefined variables
- emit
template_undefined_variablewarning diagnostics for each affected final attribute - hard-fail immediately on syntax and non-undefined render errors
- Define goal semantics explicitly:
- render graph
goalfirst and store the rendered value back onto the graph - use that rendered value as
{{ goal }}when rendering all other attributes - if
graph [goal="Demo {{ inputs.app_dir }}"]is missinginputs.app_dir, the graph goal becomesDemo, exactly one goal diagnostic is emitted, and other attributes using{{ goal }}receiveDemowithout re-emitting the missing-input diagnostic
- render graph
- Remove all source-level workflow rendering:
operations/create.rsparsesdot_sourcedirectlyImportTransformparses imported.fabrosource directly and no longer storesinputsfabro-manifestparses raw workflow source for scanning and graph-goal extractionAgentHandler/PromptHandlerconsume already-rendered prompt attributes and stop callingfabro_template
- Centralize static path-reference validation:
- add one helper, for example
validate_static_reference(value, ReferenceKind), pluscontains_template_syntax(value) - call it from manifest scanning,
FileInliningTransform,ImportTransform, and config-sourced bundled file handling - reject any path/reference containing
{{,{%, or{#}with a clear error that templates are not supported in workflow/file references
- add one helper, for example
- Add a concrete guardrail test:
- create
lib/crates/fabro-workflow/tests/template_render_call_sites.rs - recursively scan
lib/crates/**/*.rs, excludinglib/crates/fabro-template/src/lib.rs - fail if workflow-definition rendering call patterns appear outside an explicit allowlist:
render_template(,render_lenient(,render_scan_template,render as render_template,render_lenient as, or directfabro_template::{... render ...}imports - start with this allowlist:
lib/crates/fabro-workflow/src/transforms/variable_expansion.rs— the only workflow-definition rendererlib/crates/fabro-hooks/src/executor.rs— hook header/env interpolation is a separate system
- print a failure message that names the violating file and says:
Workflow template rendering must go through TemplateTransform. Add an allowlist entry only for non-workflow interpolation with a reason.
- create
Test Plan
fabro-workflowunit/regression tests:- structural missing input in a final attribute renders empty text and emits a warning diagnostic
- graph
goalwith a missing input emits exactly one diagnostic and stores the rendered goal {{ goal }}in another attribute uses the rendered graph goal without duplicate diagnostics- syntax errors hard-fail during attribute rendering
- strict callers can promote
template_undefined_variablewarnings to errors throughValidated::promote_rule_to_error(TEMPLATE_UNDEFINED_VARIABLE_RULE) ImportTransformpreserves templates inside imported graph attributes untilTemplateTransformFileInliningTransformrejects templated@filereferences before lookup
- Add an invariant test helper:
- create a workflow with prompt text inline
- create the same workflow with that prompt extracted into
@prompt.md - validate both and assert normalized diagnostics are identical
- cover at least missing input and successful
{{ goal }}interpolation
- CLI regression tests:
- imported child
.fabrowithprompt="Work in {{ inputs.app_dir }}"validates with a warning, not a hard error - run/create/preflight fail when any
template_undefined_variablediagnostic is present after promotion - source-level templating such as templated node IDs fails parse/validation instead of being rendered
- templated path references such as
prompt="@prompts/{{ inputs.prompt_file }}"fail with the new static-reference error
- imported child
fabro-manifesttests:- replace tests that expect input overrides to resolve templated prompt/import/child/goal file paths
- assert those templated references now fail with the static-reference error
- assert normal literal
@file,import, child workflow, graph goal file, and Dockerfile references still bundle correctly
- Docs/tests updates:
- update public docs that claim imported files are rendered before parse or inputs can parameterize workflow structure/paths
- update docs that claim all prompt undefined variables fail during validate; validate now warns while run-style paths fail
- keep historical docs in
docs/superpowers/plans/*unchanged unless they are part of an active test fixture
- Run:
cargo nextest run -p fabro-workflowcargo nextest run -p fabro-manifestcargo nextest run -p fabro-clicargo +nightly-2026-04-14 fmt --check --allcargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings
Assumptions
- Greenfield means source-level/structural templating can be removed without compatibility migration.
[run.inputs], CLI--input/-I, and existing input merge/override plumbing remain intact; only workflow-definition rendering timing and scope changes.- Rust APIs may change internally; no OpenAPI, TypeScript client, or command syntax change is intended.
{{ env.* }}config interpolation and hook header interpolation are separate systems and must not be changed by this work.- The final product contract is static workflow structure plus templated final string attributes.