Agent Consumer Framework Analysis
Timestamp: 2026-05-28T15:57:01+0200
Updated: 2026-05-28 after the 027-generated-evidence-workflow merge and
follow-up package/template pin commits.
This analysis evaluates FS.Skia.UI as a framework whose primary generated consumer is an autonomous agent running Spec Kit, not a human developer typing commands by hand. Under that assumption, the governance harness is not mainly a usability burden. It is part of the framework contract: it gives an agent command entry points, evidence paths, failure classes, and readiness obligations that make completion auditable. The main risks are therefore not "too many commands" or "too much ceremony" by themselves. The risks are slow routing, ambiguous failure ownership, stringly typed public control contracts, and a custom build target interpreter that duplicates parts of FAKE while remaining harder for external tools to understand.
Scope And Evidence
This document is based on local repository inspection and primary external documentation.
Local sources:
- README.md
- docs/reports/architecture.md
- docs/reports/build.md
- docs/reports/evidence.md
- docs/reports/generated-apps.md
- docs/reports/template-profile.md
- specs/027-generated-evidence-workflow/plan.md
- build.fsx
- template/base/src/Product/Program.fs
- template/profiles/app.yml
- template/profiles/governed.yml
- src/Scene/Scene.fsi
- src/SkiaViewer/SkiaViewer.fsi
- src/Controls/Types.fsi
- src/Controls/Attributes.fsi
- src/Controls/Control.fs
- src/Controls/Charts.fs
- src/Controls/DataGrid.fs
- tests/Controls.Tests
External references:
- FAKE describes itself as an F# build task DSL and supports native target dependencies between targets. Source: https://fake.build/
-
FAKE's target module documents
Target.create, dependency operators such as==>,Target.runOrDefault, listing targets,--single-target, and--parallel. Source: https://fake.build/guide/core-targets.html - Elmish describes the MVU core as UI-independent: immutable model, discriminated-union messages, pure init/update/view functions, and commands. Source: https://elmish.github.io/elmish/
Reframed Consumer Model
If the consumer is a Spec Kit agent, framework quality is measured differently than for a manually operated app starter.
Human-first questions:
- Is the generated project small?
- Can a developer understand the command list immediately?
- Is the entrypoint visually simple?
Agent-first questions:
- Can the agent determine the required proof from changed files and feature metadata?
- Can the agent run the smallest authoritative gate for the current change?
- Can failures be classified as product, environment, stale prerequisite, policy, or missing evidence without guesswork?
- Can generated evidence be re-created, validated, and cited?
- Can broad validation avoid rerunning expensive matrix rows when focused evidence is sufficient?
This repository already answers several of those questions better than a minimal app template would. The current harness gives agents stable FAKE targets, readiness directories, command logs, generated product validation, focused-gate summaries, evidence graph and audit outputs, and synthetic-evidence disclosure.
The improvement target should therefore be "more deterministic and cheaper for agents" rather than "less governed".
Current Strengths
1. The Runtime Boundary Is Sound
The conceptual runtime split is strong:
|
This aligns with Elmish's UI-independent MVU description: immutable model,
message type, pure init/update/view, and commands. FS.Skia.UI does not require
product logic to own the native window, SKCanvas, GPU handles, or screenshot
machinery.
For agents, this is valuable because semantic tests can exercise reducers, view functions, scene descriptions, control diagnostics, and layout evidence without needing a reliable desktop session.
2. The Governance Harness Provides Real Agent Affordances
The FAKE target surface in build.fsx defines named operations that an agent can run and cite:
DevPackageSurfaceCheckFsiTranscriptsTemplateCheckCapabilityCheckSkillCheckGeneratedProductCheckControlsCatalogCheckControlsInteractionCheckControlsRenderingCheckDependencyReportGeneratedGuidanceCheckTemplateDriftEvidenceGraphEvidenceAuditVerifyPreflightCiPreflightStaleBoundaryScanFinalReadinessVerifyCi
The target outputs are not only pass/fail values. They also produce logs, readiness Markdown files, JSON outputs, package surfaces, generated product file lists, and focused-gate summaries. This is a strong design for an agentic workflow because it creates durable evidence instead of relying on the final assistant response.
After 027-generated-evidence-workflow, generated EvidenceGraph and
EvidenceAudit targets are no longer completion-only placeholders. Generated
projects delegate to the authoritative Spec Kit evidence script, record
authority=delegated-authoritative, surface the generated project identity,
exit code, validation area, diagnostics, and write command reports under
readiness/.
Focused gates also have stronger in-repository metadata than the original analysis observed: command, direct prerequisites, log path, readiness path, stale build/restore assumptions, and verdict category are represented in the build workflow and documented in docs/reports/build.md. This improves direct invocation and evidence review, but it is still not a changed-path validation manifest.
3. Unsupported Host Behavior Is Treated As A Contract
The generated app and viewer evidence surfaces distinguish:
- persistent window launch
- bounded smoke
- deterministic scene evidence
- screenshot proof
- pixel readback
- unsupported desktop host
- product defect
- package resolution failure
That distinction matters for agents. Without it, an agent often treats a CI or desktop-session limitation as a product failure, or worse, relabels a fallback artifact as visual proof.
4. Generated Consumers Validate Packages, Not Source Copies
The generated consumer process uses local NuGet packages from PackLocal.
That is the right contract for a framework. It verifies the public package
surface a real generated product will consume, not repository internals.
5. Public .fsi Files Make Surface Review Concrete
The paired .fsi style creates a reviewable contract. This is especially
important for an agent-driven framework because agents tend to add helper
functions opportunistically. The .fsi boundary makes public API expansion
visible.
Main Risks
1. Agent Routing Is Too Implicit
The repository documents many gates, but the gate selection rule is not yet a first-class machine-readable contract. An agent can read docs/reports/build.md, but it still has to infer which gates are authoritative for a given change.
Example: a change under src/Controls may need some subset of:
DevControlsCatalogCheckControlsInteractionCheckControlsRenderingCheckPackageSurfaceCheckFsiTranscriptsGeneratedProductCheckEvidenceGraphEvidenceAuditVerify
Those requirements are understandable to a maintainer, but an agent needs an explicit mapping from changed paths and feature risk to gates.
Risk: the agent either over-runs expensive broad validation or under-runs the proof needed for final readiness.
2. Broad Validation Has High Latency And Large Failure Surface
Verify depends on many targets, including template validation, generated
product validation, dependency checks, guidance checks, drift checks, graph
generation, and audit. That is appropriate for final authority, but it is a
poor default inner loop.
The problem is not that broad validation exists. The problem is that without a manifested routing contract, broad validation becomes the only obviously safe choice for an agent that wants to avoid missing proof.
Risk: the agent spends most of its time proving unrelated matrix rows, and environment-sensitive failures obscure product defects.
3. Some Generated App Policy Lives In The Product Entrypoint
template/base/src/Product/Program.fs contains the normal product launch path plus many evidence modes:
--layout-evidence--launch-evidence--bounded-smoke--bounded-smoke-frame-diagnostics--scene-evidence--window-diagnostics--window-options--image-evidence--screenshot-evidence--pixel-readback-evidence
This is not a human burden in the agent-consumer model, but it is still an architecture risk. The product executable becomes both app and policy harness. That makes the generated product's command-line surface part of governance compatibility.
Risk: evidence policy evolution forces generated app churn and increases the chance that a product command accidentally claims stronger proof than it has.
4. Controls Are Productive But Stringly Typed Internally And Publicly
The Controls surface exposes convenient typed modules such as Button,
TextBox, LineChart, and DataGrid, but the underlying contract is largely
string based:
type ControlId = string
type ControlKind = string
type Control<'msg> =
{ Kind: ControlKind
Key: ControlId option
Attributes: Attr<'msg> list
Children: Control<'msg> list
Content: string option
Accessibility: AccessibilityMetadata option }
and Attr<'msg> =
{ Name: string
Category: AttrCategory
Value: AttrValue<'msg> }
Attr.create accepts arbitrary names. Event kinds are strings. Several data
paths use UntypedValue of obj. ControlInternals.required maps required
attributes from string control kinds to string attribute names. Event binding
normalization maps string attribute names such as onClick and onChanged to
event names such as click and changed.
This design is flexible and simple to extend. It is also weakly typed at the point where agents most need compile-time guardrails.
Observed examples:
Attr.on: eventKind: string -> msg: 'msg -> Attr<'msg>Attr.onWith: eventKind: string -> (ControlEvent -> 'msg) -> Attr<'msg>Control.create: kind: ControlKind -> Attr<'msg> list -> Control<'msg>- chart values stored through
UntypedValue - DataGrid columns, rows, and visible range stored through
UntypedValue
Risks:
- misspelled attribute names become diagnostics or missing behavior instead of compiler errors
- custom controls can bypass expected required attributes
- event names become a compatibility vocabulary without a typed owner
- agents may generate superficially valid controls with stale string keys
- surface baselines catch public names, but not semantic misuse of string attribute names
5. Build Graph Duplicates FAKE Target Infrastructure
The current build.fsx implements a custom target system:
BuildModelBuildMsgBuildEffectupdaterequiredTargetstargetDependenciesrunWithDependenciestargetFromArgs- effect interpreter
FAKE already provides target definition, dependency operators, target listing, single-target execution, and parallel traversal support through its target module. The repository's custom layer buys something useful: a pure transition model that can be tested and a single place to emit structured reports. But it also means external FAKE conventions are bypassed.
Risks:
fake build --listdoes not naturally list trueTarget.createtargets-
--single-target, native target arguments, FAKE context, build status, final targets, build-failure targets, and parallel options are either absent or reimplemented - target names are string literals in several places
- dependencies are not typed and can drift from docs
- agents familiar with FAKE need repo-specific routing rules
- the custom runner becomes another framework to maintain
This is not automatically wrong. It is a deliberate tradeoff: testable build workflow algebra versus idiomatic FAKE integration. The current implementation leans heavily toward the custom algebra.
Recommendations
Recommendation 1: Add An Agent Validation Manifest
Add a machine-readable validation contract, for example:
|
The manifest should map path patterns, capability ids, and feature risk levels to required gates, expected artifacts, timeout class, and failure ownership.
Sketch:
|
Agent benefit:
- the agent can choose the smallest authoritative gate
- final readiness can cite why a gate was required
- gate omissions become validation failures instead of reviewer discoveries
Recommendation 2: Introduce Validation Tiers
Keep the existing target names, but formalize tier semantics.
Suggested tiers:
Tier |
Purpose |
Example targets |
|---|---|---|
|
fast product correctness |
|
|
authoritative proof for one changed concern |
|
|
minimum feature-complete proof selected by manifest |
path-derived focused gates plus |
|
broad repository confidence |
|
|
non-interactive final authority |
|
The new tier worth adding is AgentReady. It should not be a static clone of
Verify. It should read the validation manifest and run only required focused
gates plus evidence graph/audit.
Recommendation 3: Produce One Consolidated Agent Verdict
Every focused and broad validation path should converge into one machine-readable file, for example:
|
Minimum fields:
|
This should supplement, not replace, the existing Markdown evidence. Agents need a compact routing artifact, while reviewers still benefit from readable reports.
Recommendation 4: Keep Evidence Commands, But Move Policy Out Of Product Main
Generated products need evidence commands. They should not have to own all
evidence policy in Program.fs.
Recommended direction:
-
keep product-owned facts in product modules:
viewupdategeneratedHost- layout facts
- key mapping
-
move command orchestration and report formatting into:
FS.Skia.UI.Testing- generated
Product.Evidencemodule - FAKE targets
Preferred generated shape:
|
This keeps generated product code testable while reducing policy duplication inside the app entrypoint.
Recommendation 5: Gradually Type The Controls Contract
Do not remove the flexible Control and Attr representation immediately. It
is useful for generic rendering, diagnostics, catalogs, and generated controls.
Instead, add typed front doors while preserving the existing representation as
the lowered form.
5.1 Add Typed Control Kinds
Replace public ControlKind = string usage at creation sites with a union or
single-case wrapper plus known values.
Option A: discriminated union:
type ControlKind =
| TextBlock
| Label
| Button
| TextBox
| DataGrid
| Chart of ChartKind
| Custom of string
Option B: opaque wrapper:
type ControlKind = private ControlKind of string
module ControlKind =
val button: ControlKind
val textBox: ControlKind
val custom: string -> ControlKind
val value: ControlKind -> string
Option B is less disruptive because it preserves custom extensibility and string output for catalogs.
5.2 Add Typed Event Kinds
Introduce:
type ControlEventKind =
| Click
| Changed
| Selected
| TextCommitted
| CustomEvent of string
Then expose:
module Attr =
val onEvent: ControlEventKind -> 'msg -> Attr<'msg>
val onEventWith: ControlEventKind -> (ControlEvent -> 'msg) -> Attr<'msg>
Keep Attr.on and Attr.onWith as compatibility APIs, but have typed modules
use ControlEventKind.
5.3 Replace Common UntypedValue Paths
The highest-value typed attributes are chart and DataGrid data:
type AttrValue<'msg> =
| TextValue of string
| BoolValue of bool
| FloatValue of float
| StringListValue of string list
| ChartSeriesValue of ChartSeries list
| ChartPointValue of ChartPoint list
| DataGridColumnsValue of DataGridColumn list
| DataGridRowsValue of DataGridRow list
| VisibleRangeValue of VisibleRange
| MessageValue of 'msg
| EventValue of (ControlEvent -> 'msg)
| CustomValue of obj
Then keep CustomValue only for genuine extension cases.
5.4 Add A Control Schema Registry
Today required attributes are in ControlInternals.required as string matches.
Move this into a schema table:
type ControlAttributeRequirement =
{ Name: AttributeName
Category: AttrCategory
Required: bool }
type ControlSchema =
{ Kind: ControlKind
RequiredAttributes: ControlAttributeRequirement list
SupportedEvents: ControlEventKind list
AccessibilityRole: AccessibilityRole }
Agent benefit:
- generated controls can be validated against schema before rendering
- missing attributes can name typed requirements
- catalogs, docs, and diagnostics share one source of truth
5.5 Keep The Lowered Form
The renderer can still consume:
Control<'msg>
Attr<'msg>
AttrValue<'msg>
The goal is not to make the renderer generic over many typed control records. The goal is to give agents typed constructors that lower into the existing representation.
Recommendation 6: Use Native FAKE Targets
The chosen build-graph direction is native FAKE target registration with pure planning functions retained for testability. The repository should stop treating the current custom traversal as a long-term target interpreter and move target ownership back to FAKE.
Register targets through FAKE:
Target.create "Dev" (fun _ -> runEffects (planTarget "Dev"))
Target.create "Verify" (fun _ -> runEffects (planTarget "Verify"))
open Fake.Core.TargetOperators
"Restore" ==> "Build" ==> "Test" ==> "Dev"
"EvidenceGraph" ==> "EvidenceAudit"
Keep pure functions:
val planTarget: BuildModel -> TargetId -> BuildEffect list
val targetMetadata: TargetId -> TargetMetadata
Add explicit machine-readable metadata alongside native registration:
- target id
- description
- tier
- dependencies
- direct prerequisites
- output artifacts
- stale assumptions
- timeout class
- failure owner
Expected benefits:
- restores native FAKE conventions
- enables
--list,--single-target, target arguments, and FAKE context - reduces custom traversal code
- preserves testability of target planning
- makes the validation manifest and target metadata easier for external tools to reconcile with the actual runnable target graph
Migration constraints:
- medium migration
- governance tests around custom traversal need updates
- target metadata must be kept in sync with FAKE registrations unless generated
- current command wrappers must stay stable while target registration changes
Recommendation 7: Add Gate Cost And Authority Metadata
Agents need to reason about cost and authority. Add metadata such as:
|
This is more useful to an agent than prose alone.
Recommendation 8: Make Environment Failures First-Class In The Verdict
The current docs already distinguish environment-failure. Extend that into
every focused gate and generated product row.
Important fields:
runner_stagedesktop_session_requiredhost_requirementunsupported_host_reasonstale_prerequisiterecommended_rerun_environmentnext_commandproduct_code_touched
Agent benefit:
- avoids editing product code when the runner is degraded
- makes retries intentional
- makes unsupported visual paths non-authoritative by construction
Proposed Implementation Sequence
Phase 1: Native FAKE Registration And Metadata
Register existing targets with native FAKE Target.create definitions while
keeping pure planning functions for effect tests. Add validation.contract.yml
and target metadata. Teach existing GeneratedGuidanceCheck or a new focused
gate to validate:
- every documented target has metadata
- every metadata target has a native FAKE registration
- every focused gate declares outputs
- every changed path class maps to at least one validation rule
VerifyandCiare not required as focused-gate prerequisites
Command names and wrappers remain stable in this phase.
Phase 2: AgentReady Target
Add:
|
AgentReady should:
- read the validation contract
- compute changed-path rules from git diff or active feature metadata
- run required focused gates
- run
EvidenceGraph - run
EvidenceAudit - write
readiness/agent-verdict.json
If changed-path detection is unavailable, it should degrade explicitly and name the broad fallback command.
Phase 3: Typed Controls Front Door
Add typed wrappers without removing compatibility APIs:
ControlKindwrapper or unionAttributeNamewrapperControlEventKind- typed chart and DataGrid attr values
- schema registry
Update public module constructors first:
ButtonTextBoxCheckBoxSliderTabsMenuLineChartDataGrid
Keep Attr.create for advanced/custom cases, but move generated template code
to typed constructors.
Phase 4: Remove Custom Traversal
After native FAKE registration has covered the target graph, remove or reduce
the custom targetDependencies and runWithDependencies traversal. Generate
docs from target metadata or validate metadata against FAKE registrations so
the documented graph and runnable graph cannot drift.
Recommendation Summary
The current architecture is directionally right for a Spec Kit agent. The framework should not discard the harness. Instead, it should make the harness more machine-routable.
Highest-value changes:
- Add a validation manifest that maps paths and capabilities to required gates.
-
Add
AgentReadyas a manifest-driven middle tier between focused gates and fullVerify. -
Emit one compact
agent-verdict.jsonfor routing, authority, and next action. - Move generated evidence policy out of the product entrypoint where possible.
- Add typed Controls front doors while keeping the current lowered representation.
- Move the build graph to native FAKE targets while preserving pure planning functions for tests.
These changes preserve the core advantage of FS.Skia.UI for agent consumers: the framework can prove its own generated products. The improvement is to make that proof cheaper, more deterministic, and less dependent on implicit maintainer knowledge.
val string: value: 'T -> string
--------------------
type string = System.String
val float: value: 'T -> float (requires member op_Explicit)
--------------------
type float = System.Double
--------------------
type float<'Measure> = float