<?xml version="1.0" encoding="UTF-8" standalone="no"?><rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" version="2.0">

<channel>
	<title>Jesse Liberty - Silverlight Geek</title>
	<atom:link href="https://jesseliberty.com/feed/" rel="self" type="application/rss+xml"/>
	<link>https://jesseliberty.com</link>
	<description>More Signal - Less Noise</description>
	<lastBuildDate>Wed, 28 Oct 2026 13:38:26 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.3</generator>

<image>
	<url>https://jesseliberty.com/wp-content/uploads/2026/07/cropped-Square-Headshot-32x32.jpg</url>
	<title>Jesse Liberty</title>
	<link>https://jesseliberty.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Agentic Loops vs. Spec-Driven Development</title>
		<link>https://jesseliberty.com/2026/10/28/agentic-loops-vs-spec-driven-development/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Wed, 28 Oct 2026 13:35:44 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[Essentials]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13911</guid>

					<description><![CDATA[There are a lot of new terms being tossed around the AI developer world. Two new ones are Agentic Loops and Spec-Driven Development. This blog post will endeavor to clarify&#8230; AI-driven software development today divides into two complementary but distinct &#8230; <a href="https://jesseliberty.com/2026/10/28/agentic-loops-vs-spec-driven-development/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">There are a lot of new terms being tossed around the AI developer world. Two new ones are <strong>Agentic Loops</strong> and <strong>Spec-Driven Development</strong>. This blog post will endeavor to clarify&#8230;</p>



<p class="wp-block-paragraph">AI-driven software development today divides into two complementary but distinct patterns: agentic loop programming (autonomous, iterative agents) and Spec‑Driven Development (SDD, spec-as-source-of-truth). Both appear in modern toolchains: agentic loops power autonomous workflows and multi‑step reasoning, while SDD tames AI generation by anchoring code to machine‑readable contracts. This post compares the approaches, highlights tradeoffs, and provides compact, runnable C# examples you can adapt.</p>



<figure class="wp-block-image size-full is-resized"><img fetchpriority="high" decoding="async" width="773" height="720" src="https://jesseliberty.com/wp-content/uploads/2026/10/boxing.jpg" alt="" class="wp-image-13914" style="aspect-ratio:1.0736122586945092;width:417px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/10/boxing.jpg 773w, https://jesseliberty.com/wp-content/uploads/2026/10/boxing-150x140.jpg 150w, https://jesseliberty.com/wp-content/uploads/2026/10/boxing-300x279.jpg 300w, https://jesseliberty.com/wp-content/uploads/2026/10/boxing-768x715.jpg 768w" sizes="(max-width: 773px) 100vw, 773px" /></figure>



<span id="more-13911"></span>



<p class="wp-block-paragraph"><strong>Short definitions</strong></p>



<ul class="wp-block-list">
<li>Agentic loop programming: goal → orchestrator →reason/plan → act (tools, etc.) → observe → orchestrator, loop where an LLM-driven agent runs multiple cycles, calls tools, observes results, and continues until the goal is met. Useful for autonomy, complex orchestration, and long‑horizon tasks.</li>



<li>Spec‑Driven Development (SDD): the machine‑readable spec is the primary artifact (single source of truth). Code (or tests) is generated/derived from the spec; at the end validates that implementations match the spec. SDD improves traceability, auditability, and reduces drift in AI-generated code.</li>
</ul>



<p class="wp-block-paragraph"><strong>When to choose which</strong></p>



<ul class="wp-block-list">
<li>Choose agentic loops when you need autonomy or multi‑step decision-making (automated repair, orchestration, continuous monitoring). Expect some non‑determinism but gain flexibility and problem-solving capability.</li>



<li>Choose SDD when determinism, correctness, and traceability matter (regulated domains, contracts, API-first development). SDD reduces drift and aids audit and compliance.</li>
</ul>



<p class="wp-block-paragraph">Agentic loop: risks and hardening<br />Main risks: goal drift, infinite loops, runaway cost/latency, security/tool misuse, auditability gaps. Mitigations: typed tool I/O, explicit stop conditions, iteration/cost caps, human checkpoints for high‑impact actions, rich logging/auditing, and canary testing. These patterns are essential before deploying agents to production</p>



<p class="wp-block-paragraph">Spec‑Driven Development: risks and hardening<br />Main risks: over‑specification and maintenance burden; outdated or low‑quality specs yield brittle generation. Mitigations: invest in good spec discipline, machine‑readable formats (JSON, OpenAPI), auto‑derived tests, gates, and schema validation. SDD improves auditability because the spec itself is the contract.</p>



<p class="wp-block-paragraph"><strong>Concrete C# examples</strong><br />Below are compact patterns you can drop into a sandbox. The LLM calls are mocked — replace them with your LLM client (OpenAI/Anthropic) in real projects.</p>



<ol class="wp-block-list">
<li>Agentic loop pattern (C#)<br />Purpose: show a simple agent loop that (a) reasons via LLM placeholder, (b) executes typed tools, (c) observes and updates memory, and (d) uses explicit stop conditions.</li>
</ol>



<p class="wp-block-paragraph">Key elements: ITool contract, AgentContext (memory), clear stop conditions, max iterations, and typed tool outputs for safer parsing.</p>



<p class="wp-block-paragraph">Code (runnable pattern)</p>



<pre class="wp-block-code"><code>using System;
using System.Threading.Tasks;

public interface ITool&lt;TInput, TOutput&gt;
{
    Task&lt;TOutput&gt; ExecuteAsync(TInput input);
}

// Example tool: shell-like task
public class ShellTool : ITool&lt;string, string&gt;
{
    public Task&lt;string&gt; ExecuteAsync(string input)
    {
        // Simulate work
        return Task.FromResult($"Shell executed: {input}");
    }
}

public class AgentContext
{
    public string Goal { get; }
    public int Iteration { get; set; }
    public string Memory { get; set; }
    public decimal CostSoFar { get; set; }

    public AgentContext(string goal)
    {
        Goal = goal; Iteration = 0; Memory = ""; CostSoFar = 0m;
    }
}

public class Agent
{
    private readonly ITool&lt;string, string&gt; _tool;
    private readonly int _maxIterations;
    private readonly decimal _maxCost;

    public Agent(ITool&lt;string, string&gt; tool, int maxIterations = 10, decimal maxCost = 10.0m)
    {
        _tool = tool; _maxIterations = maxIterations; _maxCost = maxCost;
    }

    // Placeholder for LLM call; replace with actual client.
    private Task&lt;string&gt; CallLLMAsync(string prompt)
    {
        // Very simple mock that alternates between calling tool and finishing.
        if (prompt.Contains("satisfied", StringComparison.OrdinalIgnoreCase))
            return Task.FromResult("DONE");
        return Task.FromResult("ACTION:call_tool INPUT:run-step");
    }

    public async Task RunAsync(AgentContext ctx)
    {
        while (true)
        {
            if (ctx.Iteration &gt;= _maxIterations)
            {
                Console.WriteLine("Stopping: reached max iterations.");
                return;
            }

            if (ctx.CostSoFar &gt;= _maxCost)
            {
                Console.WriteLine("Stopping: reached cost budget.");
                return;
            }

            ctx.Iteration++;
            var observation = $"Goal:{ctx.Goal}; Memory:{ctx.Memory}; Iteration:{ctx.Iteration}";
            var plan = await CallLLMAsync($"Observe: {observation}\nDecide: is goal satisfied?");
            Console.WriteLine($"&#91;LLM] plan={plan}");

            if (plan.StartsWith("DONE", StringComparison.OrdinalIgnoreCase))
            {
                Console.WriteLine("Agent: Goal satisfied.");
                return;
            }

            if (plan.Contains("ACTION:call_tool", StringComparison.OrdinalIgnoreCase))
            {
                // Parse input token (naive here)
                var toolInput = "run-step";
                var toolResult = await _tool.ExecuteAsync(toolInput);
                Console.WriteLine($"&#91;Tool] {toolResult}");

                // Update memory and cost (mock cost per tool call)
                ctx.Memory += $" | iter{ctx.Iteration}:{toolResult}";
                ctx.CostSoFar += 1.0m;

                // Ask LLM to evaluate the observation
                var eval = await CallLLMAsync("Was result satisfactory?");
                if (eval.StartsWith("DONE", StringComparison.OrdinalIgnoreCase))
                {
                    Console.WriteLine("Agent: confirmed success after tool execution.");
                    return;
                }
            }
            else
            {
                Console.WriteLine("Agent: no actionable plan, stopping to avoid stray behavior.");
                return;
            }
        }
    }
}

// Example run
public static class Program
{
    public static async Task Main()
    {
        var agent = new Agent(new ShellTool(), maxIterations: 5, maxCost: 3.0m);
        var ctx = new AgentContext("Add feature X and pass unit tests");
        await agent.RunAsync(ctx);
    }
}</code></pre>



<p class="wp-block-paragraph">Notes on hardening:</p>



<ul class="wp-block-list">
<li>Replace CallLLMAsync with structured prompts and parse JSON responses. Use typed JSON schemas for actions.</li>



<li>Add RBAC for tools that access network or shell. Log each step with timestamps and full inputs/outputs for audit.</li>



<li>Implement deciders that prevent repeated cycles (dedup memory) and ephemeral human checkpoints when necessary.</li>
</ul>



<ol start="2" class="wp-block-list">
<li>Spec‑Driven Development pattern (C#)<br />Purpose: show a minimal SDD flow: a machine‑readable spec (JSON) describes endpoints; a generator creates a service skeleton and a test scaffold. In production, an AI can implement methods from the spec and CI runs auto‑derived tests to enforce the contract.</li>
</ol>



<p class="wp-block-paragraph">Example Spec (spec.json)</p>



<pre class="wp-block-code"><code>{
  "service": "UserService",
  "description": "Create and get users",
  "endpoints": &#91;
    { "name": "CreateUser", "input": { "name":"string","email":"string" }, "output": { "id":"guid" } },
    { "name": "GetUser", "input": { "id":"guid" }, "output": { "name":"string","email":"string" } }
  ],
  "constraints": {
    "email": "must be valid format",
    "name": "non-empty"
  }
}</code></pre>



<p class="wp-block-paragraph">Generator and test scaffold (simplified)</p>



<pre class="wp-block-code"><code>using System;
using System.IO;
using System.Text.Json;
using System.Text;

public class SpecGenerator
{
    public static void Generate(string specPath, string outDir)
    {
        var json = File.ReadAllText(specPath);
        using var doc = JsonDocument.Parse(json);
        var root = doc.RootElement;
        var serviceName = root.GetProperty("service").GetString();

        var sb = new StringBuilder();
        sb.AppendLine("using System;");
        sb.AppendLine("namespace Generated {");
        sb.AppendLine($"  public class {serviceName} {{");

        foreach (var ep in root.GetProperty("endpoints").EnumerateArray())
        {
            var name = ep.GetProperty("name").GetString();
            sb.AppendLine($"    // Auto-generated: {name} - implement per spec");
            sb.AppendLine($"    public object {name}(object input) =&gt; throw new NotImplementedException();");
        }

        sb.AppendLine("  } }");

        Directory.CreateDirectory(outDir);
        File.WriteAllText(Path.Combine(outDir, $"{serviceName}.cs"), sb.ToString());

        // Test scaffold example
        var test = new StringBuilder();
        test.AppendLine("using Xunit;");
        test.AppendLine($"public class {serviceName}Tests {{");
        test.AppendLine("  &#91;Fact]");
        test.AppendLine($"  public void CreateUser_ShouldReturnId() =&gt; throw new NotImplementedException();");
        test.AppendLine("}");
        File.WriteAllText(Path.Combine(outDir, $"{serviceName}.Tests.cs"), test.ToString());
        Console.WriteLine(\"Generated files in: \" + outDir);
    }

    public static void Main(string&#91;] args)
    {
        Generate(\"spec.json\", \"out\");
    }
}</code></pre>



<p class="wp-block-paragraph">How AI fits in SDD:</p>



<ul class="wp-block-list">
<li>The spec is authoritative. An AI (or human) generates code that implements the spec. Tests are generated from the spec (asserting types, constraints, and examples). CI runs tests; failures trigger regeneration or human review. This establishes traceability and reduces drift</li>
</ul>



<p class="wp-block-paragraph">Making SDD production-quality</p>



<ul class="wp-block-list">
<li>Use a formal schema (OpenAPI, JSON Schema, protobuf). Generate strong typed models (C# records/classes) from the spec. Use auto‑derived contract tests that validate constraints (email format, non-empty name). For more advanced workflows, integrate Roslyn-based analyzers that flag divergences at compile time, and store specs in a versioned repository for audit.</li>
</ul>



<p class="wp-block-paragraph"><strong>Practical comparisons and recommendations</strong></p>



<ul class="wp-block-list">
<li>Observability &amp; Auditability
<ul class="wp-block-list">
<li>Agentic loop: requires explicit structured logging, typed tool outputs, and a chain-of-thought or step tracing. Without this, auditing is difficult.</li>



<li>SDD: natural audit trail because the spec is the contract; tests and commit history show intent and evidence.</li>
</ul>
</li>



<li>Speed vs Overhead
<ul class="wp-block-list">
<li>Agentic loops can rapidly iterate and adapt but add runtime complexity (guardrails, tool contracts).</li>



<li>SDD requires upfront work on a machine‑readable spec but reduces downstream rework and improves compliance.</li>
</ul>
</li>



<li>Typical real-world use-cases
<ul class="wp-block-list">
<li>Agentic loops: automated bug-fix agents, cloud orchestration that runs multi-step operations, autonomous data pipelines, or customer support agents that call external APIs.</li>



<li>SDD: API-first microservices in finance/healthcare, contract-based integrations, teams generating implementations from an agreed, audited spec.</li>
</ul>
</li>



<li>Combining the two (hybrid patterns)
<ul class="wp-block-list">
<li>Use SDD to define critical contracts and expose typed tool interfaces to agents. Agents operate inside the guardrails defined by the spec. For example, an agent that patches code should only call tools whose inputs/outputs are specified in SDD artifacts. This approach balances autonomy with control and auditability.</li>
</ul>
</li>
</ul>



<p class="wp-block-paragraph">Conclusion<br />Agentic loop programming and Spec‑Driven Development serve different needs. Agentic loops buy autonomy and multi-step problem solving. SDD buys determinism, traceability, and compliance. In practice, the strongest architectures blend them: use SDD to define and enforce the boundaries and contracts, and deploy agentic loops inside those boundaries to perform autonomous tasks. The C# patterns above give you a starting point: a simple agent loop pattern hardened with stop conditions and typed tools, and an SDD generator that shows how specs map to code and tests<br /><br />Note: Much of the code was written by AI and is offered as is (AI makes mistakes, so proceed with caution). The text was also written by AI but significantly edited by me.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Prompts are dead; long live loops!</title>
		<link>https://jesseliberty.com/2026/10/26/prompts-are-dead-long-live-loops/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Mon, 26 Oct 2026 19:51:03 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13906</guid>

					<description><![CDATA[As you may have heard, loops are the hottest thing in AI. The buzz is that rather than you writing prompts, you let your agent do so. The problem is that while all the videos I&#8217;ve seen are great at &#8230; <a href="https://jesseliberty.com/2026/10/26/prompts-are-dead-long-live-loops/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">As you may have heard, loops are the hottest thing in AI. The buzz is that rather than you writing prompts, you let your agent do so. The problem is that while all the videos I&#8217;ve seen are great at touting the advantages and even explaining the moving parts (though each one has slightly different architecture), I&#8217;ve yet to see one that gives concrete C# examples—let alone using the Microsoft Agent Framework.</p>



<figure class="wp-block-image size-large is-resized"><img decoding="async" width="800" height="340" src="https://jesseliberty.com/wp-content/uploads/2026/10/loop-800x340.jpg" alt="" class="wp-image-13907" style="aspect-ratio:2.353070295780058;width:412px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/10/loop-800x340.jpg 800w, https://jesseliberty.com/wp-content/uploads/2026/10/loop-150x64.jpg 150w, https://jesseliberty.com/wp-content/uploads/2026/10/loop-300x127.jpg 300w, https://jesseliberty.com/wp-content/uploads/2026/10/loop-767x326.jpg 767w, https://jesseliberty.com/wp-content/uploads/2026/10/loop.jpg 951w" sizes="(max-width: 800px) 100vw, 800px" /></figure>



<p class="wp-block-paragraph">Soooo, this is the very first in a series. I&#8217;m finding my way, stumbling in the dark, so some of this may be a bit simplistic—but I believe it is correct as far as it goes. Remember, however, that  the first draft of this post was written by AI. I&#8217;ve edited it, but your mileage may vary.</p>



<span id="more-13906"></span>



<p class="wp-block-paragraph">Here&#8217;s a thought experiment. Imagine having an agent that keeps trying fixes until a web service stays healthy—retrying configuration changes, re-checking status, and stopping only when a health check returns OK or a safety limit is reached. That behavior—automatic, bounded repetition with an evaluation step between attempts—is exactly what loop programming enables in modern agentic AI. This post explains the pattern, why it matters, and how to implement it in C# using the Microsoft Agent Framework (MAF) concepts. </p>



<p class="wp-block-paragraph"><strong>One-sentence takeaway</strong><br />Loop programming = agent acts → observes → evaluates → repeats (until accepted or bound). Use MAF’s loop primitives (loop wrapper or middleware + evaluators) to implement this pattern safely in .NET.</p>



<p class="wp-block-paragraph"><strong>Why loop programming matters</strong></p>



<ul class="wp-block-list">
<li>Iterative refinement: reduce hallucinations by asking the model to rework outputs until they meet quality criteria.</li>



<li>Multi-step tasks: break complex tasks into incremental attempts (repair, check, retry).</li>



<li>Automation &amp; robustness: automatically retry flaky tools with idempotency and backoff, or orchestrate multi-tool workflows until success.</li>



<li>Cost/latency tradeoffs: each iteration consumes time and tokens—a deliberate loop strategy balances accuracy and cost.</li>
</ul>



<p class="wp-block-paragraph"><strong>Core MAF loop terms</strong></p>



<ul class="wp-block-list">
<li>Base agent / client: This is the model wrapped as an agent (an abstraction that receives requests and produces responses).</li>



<li>Loop wrapper or middleware: A piece that re-invokes the base agent automatically, applying evaluator logic between iterations.</li>



<li>Evaluator: A component that inspects the previous response and returns Continue or Stop. Examples:
<ul class="wp-block-list">
<li>Marker-based evaluator: stop when the output contains a specific marker (e.g., &#8220;DONE&#8221;).</li>



<li>Judge evaluator: ask an auxiliary judge model to say whether the output is acceptable.</li>



<li>Custom evaluator: your own logic that checks outputs, tool results, or external state.</li>
</ul>
</li>



<li>Bounds: MaxIterations, timeouts, and cancellation tokens—mandatory to avoid runaway loops.</li>



<li>AgentRequest: the input to an agent (prompt, tools, options).</li>



<li>AgentResponse: the output (text, structured data, tool results).</li>
</ul>



<p class="wp-block-paragraph"><strong>Minimal, runnable-style C# example</strong> <br />Below is a full Program.cs and a simple csproj. The example uses a completion-marker evaluator (&#8220;DONE&#8221;) and a MaxIterations bound. It is a bit brain-dead, but it will give you a sense of how to string one of these together.</p>



<p class="wp-block-paragraph"><br /></p>



<pre class="wp-block-preformatted">using System;<br />using System.Threading;<br />using System.Threading.Tasks;<br /><br />using Microsoft.Agents.AI;<br />using Microsoft.Agents.AI.Evaluators;<br />using Microsoft.Agents.AI.OpenAI.Clients;<br /><br />class Program<br />{<br />   static async Task Main(string[] args)<br />   {<br />      // Read API key (set as env var)<br />      string? apiKey =     Environment.GetEnvironmentVariable("OPENAI_API_KEY");<br />      if (string.IsNullOrEmpty(apiKey))<br />      {<br />         Console.WriteLine("Set OPENAI_API_KEY environment variable and re-run.");<br />         return;<br />      }<br /><code> // 1) Create provider client (provider-specific)<br />    var openAiClient = new OpenAiProviderClient(new OpenAiOptions { ApiKey = apiKey });<br /><br />    // 2) Convert provider client to an agent abstraction<br />    AIAgent baseAgent = openAiClient.AsAIAgent();<br /><br />    // 3) Create a simple marker evaluator that stops when "DONE" appears<br />    var evaluator = new CompletionMarkerLoopEvaluator("DONE");<br /><br />    // 4) Wrap the base agent with loop behavior and bounds<br />    var loopOptions = new LoopAgentOptions { MaxIterations = 5 };<br />    AIAgent loopAgent = new LoopAgent(baseAgent, evaluator, loopOptions);<br /><br />    // 5) Prepare request<br />    var request = new AgentRequest<br />    {<br />        Prompt = "Summarize this paragraph and keep refining until you append DONE:\n\n" +<br />                 "The quick brown fox jumps over the lazy dog. This sentence includes all the letters of the English alphabet."<br />    };<br /><br />    // 6) Use a timeout for safety<br />    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));<br />    try<br />    {<br />        AgentResponse response = await loopAgent.InvokeAsync(request, cts.Token);<br />        Console.WriteLine("Final response:\n" + response.Text);<br />    }<br />    catch (OperationCanceledException)<br />    {<br />        Console.WriteLine("Operation timed out or was canceled.");<br />    }<br />}<br /></code>}</pre>



<p class="wp-block-paragraph">How to adapt this to the real MAF packages</p>



<ul class="wp-block-list">
<li>Install packages
<ul class="wp-block-list">
<li>dotnet add package Microsoft.Agents.AI</li>



<li>dotnet add package Microsoft.Agents.AI.OpenAI</li>
</ul>
</li>



<li>The logical steps are always pretty much the same: build a base agent, choose an evaluator, apply loop wrapper or middleware, and call it.</li>
</ul>



<p class="wp-block-paragraph">Designing better evaluators (AI judge and custom evaluator)</p>



<p class="wp-block-paragraph"><br /><strong>Marker evaluator (simple)</strong></p>



<ul class="wp-block-list">
<li>Pros: deterministic and cheap.</li>



<li>Cons: brittle.</li>
</ul>



<p class="wp-block-paragraph"><strong>AIJudge evaluator (higher quality)</strong></p>



<ul class="wp-block-list">
<li>Pattern: after each iteration, call a small judge prompt that returns YES/NO and a short reason. Use a very explicit evaluation prompt to reduce ambiguity.</li>



<li>Example judge prompt (send as a separate small completion request):<br />&#8220;You are a strict grader. Given the task: [task description]. Given the model response: [response]. Is the response acceptable? Answer EXACTLY YES or NO followed by a short reason.&#8221;</li>
</ul>



<p class="wp-block-paragraph"><strong>Custom evaluator pattern (checks content + side effects)</strong></p>



<ul class="wp-block-list">
<li>Inspect response text for task markers (like DONE) and for required structured outputs.</li>



<li>Check external state (database, API) to determine if the side effect succeeded.</li>



<li>Optionally consult a judge model only when heuristic checks are inconclusive.</li>
</ul>



<p class="wp-block-paragraph">Sample custom evaluator </p>



<pre class="wp-block-code"><code>public class KeywordOrJudgeEvaluator : ILoopEvaluator
{
private readonly string _stopKeyword;
private readonly AIAgent? _judgeAgent;

public KeywordOrJudgeEvaluator(string stopKeyword, AIAgent? judgeAgent = null)
{
    _stopKeyword = stopKeyword;
    _judgeAgent = judgeAgent;
}

public async Task&lt;LoopDecision> EvaluateAsync(AgentResponse lastResponse, CancellationToken ct)
{
    if (lastResponse.Text?.Contains(_stopKeyword, StringComparison.OrdinalIgnoreCase) == true)
        return LoopDecision.Stop();

    if (_judgeAgent != null)
    {
        var judgeRequest = new AgentRequest
        {
            Prompt = $"Is the following response acceptable? Answer YES or NO.\n\nResponse:\n{lastResponse.Text}\n\nCriteria: &#91;list your criteria]"
        };
        var judgeResponse = await _judgeAgent.InvokeAsync(judgeRequest, ct);
        if (judgeResponse.Text.Trim().StartsWith("YES", StringComparison.OrdinalIgnoreCase))
            return LoopDecision.Stop();
    }

    return LoopDecision.Continue();
}
}</code></pre>



<p class="wp-block-paragraph"><strong>Practical safety and production best practices</strong></p>



<ul class="wp-block-list">
<li>Always set MaxIterations. Decide MaxIterations based on expected task complexity (2–5 for refinement tasks, more for longer repair flows).</li>



<li>Use a wall-clock timeout (CancellationTokenSource) to cap total runtime.</li>



<li>Log each iteration: input prompt, model output, evaluator decision, iteration duration, and any tool calls.</li>



<li>Be idempotent for side-effecting tool calls: include an idempotency key or track completed steps to avoid duplicate side effects.</li>



<li>Use backoff and limited retries for transient tool failures.</li>



<li>Consider human-in-the-loop for high-risk operations.</li>



<li>Capture metrics: iterations-per-task, average cost per task, success rate after N iterations.</li>
</ul>



<p class="wp-block-paragraph">Exponential backoff snippet</p>



<pre class="wp-block-code"><code>int attempt = 0;
while (attempt &lt; maxRetries)
{
   attempt++;
   try { callTool(); break; }
   catch (TransientException)
   {
      await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
   }
}</code></pre>



<p class="wp-block-paragraph"><strong>Cost and latency tradeoffs</strong></p>



<ul class="wp-block-list">
<li>Each loop iteration = another model call (cost + latency). If you expect 2–3 iterations on average, estimate cost = baseCost × expectedIterations + judgeCost (if used).</li>



<li>Use marker evaluators when iterations are cheap and you can control the prompt reliably; use judge evaluators when marker brittleness is likely.</li>



<li>For low-latency needs, constrain MaxIterations or move part of the evaluation logic to a fast heuristic.</li>
</ul>



<p class="wp-block-paragraph"><strong>Debugging, testing, and observability tips</strong></p>



<ul class="wp-block-list">
<li>Unit test evaluators with mocked AgentResponse objects. Keep evaluator logic pure when possible so it’s easy to test.</li>



<li>Mock provider clients for integration tests so you can simulate iteration sequences and failures deterministically.</li>



<li>Log a concise iteration record: {iteration, promptFingerprint, outputHash, evaluatorDecision, duration, costEstimate}.</li>



<li>Add correlation IDs across iterations for traceability.</li>
</ul>



<p class="wp-block-paragraph"><strong>Troubleshooting common failure modes</strong></p>



<ul class="wp-block-list">
<li>Marker never appended → verify prompt instruction clarity, or switch to judge evaluator.</li>



<li>Judge flip-flops (YES/NO across iterations) → add hysteresis: require two consecutive YES judgments, or use a heuristic plus judge.</li>



<li>Side effects duplicated → make tools idempotent or track operation IDs.</li>



<li>Loop stalls (no progress) → add progress metrics and a stuck detection rule (stop if no semantic change after N iterations).</li>
</ul>



<p class="wp-block-paragraph"><strong>Real-world scenario examples </strong></p>



<ul class="wp-block-list">
<li>Document extraction and QA: agent extracts structured data, runs validators, requests corrections until extraction passes validators or MaxIterations reached.</li>



<li>Customer support automation: agent composes reply drafts and refines them until a judge model or human reviewer marks them acceptable.</li>
</ul>



<p class="wp-block-paragraph"><strong>Conclusion and next steps</strong><br />Loop programming is a practical, powerful pattern for agentic AI: agent acts, inspects results, evaluates, and repeats — but only when bounded and observable. The Microsoft Agent Framework offers loop wrapper and evaluator patterns to implement this safely; adapt the example above to your MAF version and provider.</p>



<p class="wp-block-paragraph"><strong>BlogWriter</strong></p>



<p class="wp-block-paragraph">My demo program, <a href="https://github.com/jesseliberty/blogwriter">BlogWriter</a>, uses a much more deterministic approach. While the author does pass its draft to the reviewer for evaluation, and they do create a small loop, a true looping approach would, perhaps, return better results. Something to try.</p>



<p class="wp-block-paragraph">More to come soon&#8230;</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>YouTube Player – Open Source</title>
		<link>https://jesseliberty.com/2026/10/21/youtube-player-open-source/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Wed, 21 Oct 2026 17:51:03 +0000</pubDate>
				<category><![CDATA[Open Source]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13887</guid>

					<description><![CDATA[Today I took my YouTube Player open source under the MIT license. Looking forward to the community contributions and opinions. The source offers two versions: Desktop and Web. Please read CONTRIBUTING.md before contributing to the project. For you to use &#8230; <a href="https://jesseliberty.com/2026/10/21/youtube-player-open-source/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Today I took my <a href="https://github.com/JesseLiberty/YouTubePlayer">YouTube Player</a> open source under the MIT license. Looking forward to the community contributions and opinions.<br /></p>



<figure class="wp-block-image size-large is-resized"><img decoding="async" width="800" height="487" src="https://jesseliberty.com/wp-content/uploads/2026/10/YouTube-Player-800x487.jpg" alt="" class="wp-image-13888" style="aspect-ratio:1.642723058652262;width:649px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/10/YouTube-Player-800x487.jpg 800w, https://jesseliberty.com/wp-content/uploads/2026/10/YouTube-Player-150x91.jpg 150w, https://jesseliberty.com/wp-content/uploads/2026/10/YouTube-Player-300x182.jpg 300w, https://jesseliberty.com/wp-content/uploads/2026/10/YouTube-Player-768x467.jpg 768w, https://jesseliberty.com/wp-content/uploads/2026/10/YouTube-Player-1536x935.jpg 1536w, https://jesseliberty.com/wp-content/uploads/2026/10/YouTube-Player.jpg 1734w" sizes="(max-width: 800px) 100vw, 800px" /></figure>



<ul class="wp-block-list">
<li>Marks videos as started or completed</li>



<li>Displays your progress and total time for video, along with date and other meta-data</li>



<li>Single click on card displays details</li>



<li>If details contain chapters, clicking on a chapter brings you there</li>



<li>Double-click on video and go to where you left off</li>



<li>Mark videos as important (and see all important videos listed together)</li>



<li>Delete videos from your list to avoid clutter</li>



<li>Drag and drop lists of videos to reorder</li>



<li>Drag and drop cards within list</li>
</ul>



<p class="wp-block-paragraph">The source offers two versions: Desktop and Web. Please read CONTRIBUTING.md before contributing to the project.</p>



<p class="wp-block-paragraph">For you to use this, I will need to add your email to the list of test users. Please contact me at jesseliberty@gmail.com if you are going to be submitting pull requests.</p>



<p class="wp-block-paragraph">Note: Much of the code was written by AI and is offered as is (AI makes mistakes, so proceed with caution). For more, please read the Readme.md file.</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>RAG and Microsoft Agent Framework Step By Step (2)</title>
		<link>https://jesseliberty.com/2026/10/20/rag-and-microsoft-agent-framework-step-by-step-2/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Tue, 20 Oct 2026 17:33:34 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[Essentials]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13877</guid>

					<description><![CDATA[In a previous blog post I walked through a RAG example using Microsoft Agent Framework so that we could do a semantic search against our documents. However, Bruno Capuano (Microsoft) and I went over this for my latest video with &#8230; <a href="https://jesseliberty.com/2026/10/20/rag-and-microsoft-agent-framework-step-by-step-2/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">In a <a href="https://jesseliberty.com/2026/10/01/rag-in-microsoft-agent-framework-step-by-step/">previous blog post</a> I walked through a RAG example using Microsoft Agent Framework so that we could do a semantic search against our documents.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="577" height="490" src="https://jesseliberty.com/wp-content/uploads/2026/10/semantic-Distance.jpg" alt="" class="wp-image-13880" style="aspect-ratio:1.177556682292165;width:292px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/10/semantic-Distance.jpg 577w, https://jesseliberty.com/wp-content/uploads/2026/10/semantic-Distance-300x255.jpg 300w, https://jesseliberty.com/wp-content/uploads/2026/10/semantic-Distance-150x127.jpg 150w" sizes="auto, (max-width: 577px) 100vw, 577px" /></figure>



<p class="wp-block-paragraph">However, Bruno Capuano (Microsoft) and I went over this for <a href="https://www.youtube.com/watch?v=NjUvo3A9Ab0&amp;t=1875s">my latest video with him</a>. His reaction was that what I was doing was &#8220;right&#8221; but that I was working too hard, doing things that the Microsoft.Extensions.AI.Templates would do for me—or at least make simpler. He pointed me to the <a href="https://learn.microsoft.com/en-us/dotnet/ai/quickstarts/ai-templates?tabs=dotnet-cli%2Cconfigure-visual-studio&amp;pivots=ollama">quickstart </a>for AIChatSample. </p>



<p class="wp-block-paragraph">The analysis in the quickstart is out of date, and there are significant changes. Here is an analysis of the latest version of the code. You can get the code itself from the quickstart, but note that I use llama.cpp rather than Ollama.</p>



<p class="wp-block-paragraph">The rest of this post walks through the RAG implementation in AIChatSample, a Blazor chat application that uses OpenAI-compatible endpoints provided by llama.cpp. The emphasis is on the full path: configuring separate chat and embedding clients, ingesting and chunking documents, saving vectors in SQLite, retrieving relevant chunks, and turning those results into visible citations.</p>



<h2 class="wp-block-heading"><a></a>The application at a glance</h2>



<p class="wp-block-paragraph">The main pieces are registered in <code>Program.cs</code>. The app creates an <code>IChatClient</code> for conversation and an <code>IEmbeddingGenerator&lt;string, Embedding&lt;float&gt;&gt;</code> for vector embeddings. Both are built from configured endpoint and model values using the OpenAI .NET client adapters. Although the clients use the same API style, they do different jobs and may run on separate llama.cpp servers: the chat endpoint generates responses and handles tool calls, while the embedding endpoint converts text into vectors.</p>



<p class="wp-block-paragraph">That distinction matters. RAG needs embeddings when documents are prepared and again when a user query is searched. The chat model is not the search index, and a chat-only server cannot replace the embedding service. This sample configures an all-MiniLM embedding model that produces 384-dimensional vectors. The vector schema declares that same dimension, so the model and stored collection must remain compatible.</p>



<p class="wp-block-paragraph"><code>Program.cs</code> also registers a SQLite vector store and a collection for <code>IngestedChunk</code>, then registers <code>DataIngestor</code> and <code>SemanticSearch</code> as singleton services. The document source is the app&#8217;s <code>wwwroot\Data</code> directory. This keeps the sample&#8217;s architecture compact: the same directory supplies documents to the ingestion pipeline and later supports the UI&#8217;s links back to source files.</p>



<h2 class="wp-block-heading"><a></a>Stage one: read and normalize documents</h2>



<p class="wp-block-paragraph">The RAG pipeline begins when a search is needed, not when the web host starts. In <code>Chat.razor</code>, the assistant is offered a <code>LoadDocuments</code> tool and instructed to use it before searching. The tool calls <code>SemanticSearch.LoadDocumentsAsync</code>, which starts the ingestion task the first time and retains that task for later calls. This means repeated tool invocations share the same load operation rather than starting a new one each time.</p>



<p class="wp-block-paragraph"><code>DataIngestor.IngestDataAsync</code> assembles the ingestion pipeline from three parts: a reader, a chunker, and a vector-store writer. It scans the configured directory using the <code>*.*</code> search pattern. The custom <code>DocumentReader</code> delegates Markdown to the Markdown reader and PDFs to <code>PdfPigReader</code>. Markdown is assigned the <code>text/markdown</code> media type; PDFs are handled by the PDF reader. Unsupported media types fail explicitly rather than being silently treated as plain text.</p>



<p class="wp-block-paragraph">The PDF reader opens each document with PdfPig, walks through its pages, extracts letters, groups them into words, and then groups words into text blocks. Each block becomes a paragraph element in a section labeled with the PDF page number. That page-aware structure gives the ingestion pipeline organized text to work with instead of treating a PDF as a single opaque binary file. The document reader also normalizes fully qualified identifiers to paths relative to the data directory, which is useful later when a result needs to identify its source document.</p>



<h2 class="wp-block-heading">Stage two: split the text and embed it</h2>



<p class="wp-block-paragraph">Searching an entire book or long report as one record would be imprecise and could exceed what is useful to pass to a model. AIChatSample therefore uses <code>SemanticSimilarityChunker</code> in <code>DataIngestor</code>. The chunker uses the configured embedding generator and a tokenizer (<code>TiktokenTokenizer</code> configured for <code>gpt-4o</code>) to create manageable pieces. Semantic chunking aims to keep related content together, rather than cutting text only at arbitrary fixed character boundaries. The exact chunking behavior is provided by the ingestion library, while the app supplies the model and tokenizer it should use.</p>



<p class="wp-block-paragraph">Each resulting chunk is represented by <code>IngestedChunk</code>. Its fields capture a unique key, the source <code>DocumentId</code>, the chunk&#8217;s <code>Text</code>, and an optional <code>Context</code> field. The <code>Vector</code> property returns the chunk text, which allows the vector-store integration to generate an embedding from that text. The vector property is annotated as a 384-dimensional vector with cosine distance. In effect, each stored item contains both searchable semantic representation and the original text needed to give the language model actual evidence.</p>



<p class="wp-block-paragraph">The <code>VectorStoreWriter</code> is configured with the collection name, dimension count, and distance function from the record type. It writes the pipeline&#8217;s chunks into the SQLite vector store. The writer is set to non-incremental ingestion, so the application explicitly does not configure incremental ingestion for this pipeline. Persisting the collection locally means later searches can query the indexed chunks without reconstructing an in-memory index for every question.</p>



<h2 class="wp-block-heading"><a></a>Stage three: find relevant chunks</h2>



<p class="wp-block-paragraph">When the model calls the <code>Search</code> tool, <code>Chat.razor</code> passes the user&#8217;s search phrase and an optional filename filter to <code>SemanticSearch.SearchAsync</code>. That method first awaits <code>LoadDocumentsAsync</code>, ensuring ingestion is complete before vector search proceeds. The vector collection then searches for the phrase and asks for up to the requested number of results. If a document ID was supplied, a filter restricts matches to that document; without one, search spans the collection. The chat tool requests a maximum of five results.</p>



<p class="wp-block-paragraph">Conceptually, the embedding service maps the query into the same vector space as the document chunks. The vector store compares that query vector with stored chunk vectors using cosine distance and returns the nearest matches. The application returns the matching records, including their source document IDs and text. This is semantic retrieval: the query can find conceptually similar passages even when they do not repeat the exact wording of the question.</p>



<p class="wp-block-paragraph">The top results are not themselves the final answer. They are evidence to be passed back to the model. The <code>Search</code> tool formats each result as a small XML-like fragment containing a filename and the text. This gives the model explicit source boundaries and the passage content it can use when drafting a response.</p>



<h2 class="wp-block-heading"><a></a>Stage four: let the model use tools and answer</h2>



<p class="wp-block-paragraph">The chat page constructs a system prompt that restricts the assistant to questions about retrieved information. It tells the model to load the documents, use search, and cite search results. In <code>OnInitialized</code>, the page exposes the <code>LoadDocumentsAsync</code> and <code>SearchAsync</code> methods as AI functions through <code>AIFunctionFactory</code>. <code>Program.cs</code> wires function invocation into the chat client, so the model can request these application functions during a conversation.</p>



<p class="wp-block-paragraph">This is an important distinction from a design where the app blindly retrieves a fixed set of chunks for every incoming message. Here, the model can first decide to load and search, and it can provide a search phrase and optionally a filename. The UI gives it tool descriptions and argument descriptions to guide those decisions. Once the tool returns passages, the model can compose a natural-language answer based on them. It is still the model that writes the response; retrieval supplies relevant, external context.</p>



<p class="wp-block-paragraph">Responses stream into the Blazor page through <code>GetStreamingResponseAsync</code>. The page accumulates text updates and also keeps non-text content such as function-call information in the conversation. The UI can therefore show progress such as document loading or the current search phrase while the assistant works, rather than waiting for the entire response before showing anything.</p>



<h2 class="wp-block-heading"><a></a>Stage five: citations connect an answer to its source</h2>



<p class="wp-block-paragraph">The prompt asks the model to finish with citation tags in a particular format, using a short exact quote from the retrieved result. <code>ChatMessageItem</code> looks for those tags after a response is complete. It extracts the filename and quote, then renders each as a <code>ChatCitation</code> component. The component creates a link to an appropriate viewer: Markdown opens in the Markdown viewer with a text-fragment target, while PDF opens in the PDF viewer with a search fragment.</p>



<p class="wp-block-paragraph">This makes citations more than decorative labels. A reader can follow the source filename and quote back to the underlying document. The prompt&#8217;s five-word quote limit is a small convention that encourages concise evidence anchors, while the actual source passage remains available to the model during answer generation.</p>



<p class="wp-block-paragraph">There are practical limits to note. The citation is generated by the model and parsed from its text, so the prompt guides the format but does not by itself guarantee that every quote is valid or that the answer is fully supported. Retrieval quality also depends on document extraction, chunk boundaries, embedding-model suitability, and the number of results. For production use, teams often add checks that verify a cited quote appears in the retrieved text, evaluate retrieval quality against representative questions, and add metadata filters for access control or document categories.</p>



<h2 class="wp-block-heading"><a></a>How to run and evolve the RAG sample</h2>



<p class="wp-block-paragraph">The README describes configuring two llama.cpp servers: one chat-compatible model and one embedding model that supports the OpenAI-compatible embeddings endpoint. The app needs both endpoints reachable. The collection schema in <code>IngestedChunk</code> expects 384-dimensional embeddings; changing the embedding model may require updating that dimension, deleting and recreating the local vector database, and re-ingesting the documents so query vectors and stored vectors remain compatible.</p>



<p class="wp-block-paragraph">To add another document type, extend <code>DocumentReader</code> with a reader for its media type and decide how its structure should be represented as sections and elements. To tune retrieval, measure the quality of chunking, consider metadata such as page numbers and titles, and test whether the chosen result count gives the chat model enough evidence without overwhelming the prompt. To make citations more precise, preserve useful location metadata through ingestion and include it in the result returned to the model.</p>



<p class="wp-block-paragraph">The central idea is straightforward: read documents into structured text, split that text into semantically useful chunks, embed and persist the chunks, retrieve the nearest matches for a query, and give those passages to a model that can answer and cite them. AIChatSample implements each step in a small set of services and connects the steps through model-invoked tools. Follow one question through those stages and RAG becomes less of a buzzword and more of a concrete data flow—from files in <code>Data</code> to an answer with a trail back to its evidence.</p>



<p class="wp-block-paragraph">This analysis was composed by <a href="https://github.com/jesseliberty/blogwriter">BlogWriter</a> and then edited by me. Please note that AIs can make errors and this analysis is offered as is.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Creating a Spec Kit Extension</title>
		<link>https://jesseliberty.com/2026/10/10/creating-a-spec-kit-extension/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Sat, 10 Oct 2026 18:08:53 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[Essentials]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13857</guid>

					<description><![CDATA[Want to build an extension to Spec Kit? This comprehensive guide walks you through everything you need: a ready-to-drop extension layout, a complete extension.yml manifest, a slash-command file, a lifecycle hook that runs a C# helper, a full C# console &#8230; <a href="https://jesseliberty.com/2026/10/10/creating-a-spec-kit-extension/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Want to build an extension to Spec Kit? This comprehensive guide walks you through everything you need: a ready-to-drop extension layout, a complete extension.yml manifest, a slash-command file, a lifecycle hook that runs a C# helper, a full C# console app (source + csproj), install scripts, CI to build multi-platform artifacts and package the extension, plus troubleshooting, testing, and security advice.</p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="800" height="799" src="https://jesseliberty.com/wp-content/uploads/2026/10/spec-kit-extension-2-800x799.jpg" alt="" class="wp-image-13864" style="aspect-ratio:1.001237565964601;width:483px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/10/spec-kit-extension-2-800x799.jpg 800w, https://jesseliberty.com/wp-content/uploads/2026/10/spec-kit-extension-2-150x150.jpg 150w, https://jesseliberty.com/wp-content/uploads/2026/10/spec-kit-extension-2-300x300.jpg 300w, https://jesseliberty.com/wp-content/uploads/2026/10/spec-kit-extension-2-768x767.jpg 768w, https://jesseliberty.com/wp-content/uploads/2026/10/spec-kit-extension-2.jpg 974w" sizes="auto, (max-width: 800px) 100vw, 800px" /></figure>



<p class="wp-block-paragraph">What you’ll get (artifacts shown in this guide)</p>



<ul class="wp-block-list">
<li>A consistent extension folder layout you can copy into a project-level .specify/extensions/ directory for local testing. (If your environment uses a different extension directory, replace .specify accordingly.)</li>



<li>Full extension.yml manifest.</li>



<li>A slash command file: addon/commands/speckit.extn.myfeature.md.</li>



<li>A lifecycle hook descriptor: addon/hooks/after-plan.yml and platform run wrappers.</li>



<li>A small C# console app (MyFeatureRunner) that reads a spec path and writes structured JSON to stdout.</li>



<li>Install scripts (install.sh, install.ps1) and OS wrappers (run.sh/run.ps1) to select the right binary.</li>



<li>A GitHub Actions workflow that builds platform binaries and packages a ZIP for publishing.</li>



<li>Tests (xUnit) that exercise the analyzer logic (no Python anywhere).</li>
</ul>



<span id="more-13857"></span>



<p class="wp-block-paragraph"><strong>Step 0</strong> — conventions &amp; a note on paths</p>



<ul class="wp-block-list">
<li>I’ll use &#8220;Spec Kit&#8221; consistently here and show the local extension install path as .specify/extensions/ (this is the common convention in community examples). If your tooling uses a different folder, replace .specify accordingly.</li>



<li>Slash command namespace convention: /speckit.extn..* — follow this for discoverability.</li>



<li>Hooks use placeholders like {{spec.path}}; confirm your Spec Kit version’s exact interpolation tokens and adapt if necessary.</li>
</ul>



<p class="wp-block-paragraph"><strong>Step 1</strong> — pick scope and design<br />Decide the minimal valuable feature for your first release. Example here:</p>



<ul class="wp-block-list">
<li>Add a slash command /speckit.extn.myfeature that runs an analysis on the current spec and returns structured JSON with a summary and issues. Also register an after-plan lifecycle hook that runs the same analyzer automatically when planning completes.</li>
</ul>



<p class="wp-block-paragraph">Define:</p>



<ul class="wp-block-list">
<li>Inputs: spec path (string), format (json|text), output path (optional).</li>



<li>Outputs: structured JSON (summary, issues[]), exit codes.</li>



<li>Failure modes: missing spec, parse errors, internal errors — map to clear exit codes and clear stderr messages.</li>
</ul>



<p class="wp-block-paragraph">Design outcome: a C# console app MyFeatureRunner that accepts &#8211;spec and writes JSON to stdout or an output file. Hooks call the executable with placeholders.</p>



<p class="wp-block-paragraph"><strong>Step 2</strong> — folder layout (concrete)<br />Create this structure (top-level folder is the extension package root):</p>



<p class="wp-block-paragraph">com.yourorg.myfeature/<br />extension.yml<br />README.md<br />addon/<br />commands/<br />speckit.extn.myfeature.md<br />hooks/<br />after-plan.yml<br />templates/<br />mytemplate.txt<br />bin/<br />linux/<br />MyFeatureRunner &lt;&#8211; published single-file linux binary<br />macos/<br />MyFeatureRunner &lt;&#8211; published macos binary<br />windows/<br />MyFeatureRunner.exe &lt;&#8211; published windows exe<br />run.sh &lt;&#8211; wrapper selecting right binary on Unix<br />run.ps1 &lt;&#8211; wrapper for Windows<br />src/<br />MyFeatureRunner/ &lt;&#8211; dotnet project (Program.cs, Analyzer.cs, MyFeatureRunner.csproj)<br />tests/<br />MyFeatureRunner.Tests/ &lt;&#8211; xUnit tests<br />examples/<br />sample-spec.md<br />install.sh<br />install.ps1</p>



<p class="wp-block-paragraph"><strong>Notes</strong>:</p>



<ul class="wp-block-list">
<li>Place platform-specific published binaries under addon/bin//. Include wrapper scripts (run.sh, run.ps1) that forward args to the right binary.</li>



<li>The extension.yml manifest (next) will map addon/ directories into the install target.</li>
</ul>



<p class="wp-block-paragraph"><strong>Step 3</strong> — extension manifest (full example)<br />Put this file at the extension root as extension.yml:</p>



<p class="wp-block-paragraph">name: com.yourorg.myfeature<br />version: 0.1.0<br />description: &#8220;Adds /speckit.extn.myfeature — analyzes a spec and returns structured JSON with issues.&#8221;<br />authors:</p>



<ul class="wp-block-list">
<li>name: Your Name<br />email: <a href="mailto:you@example.com">you@example.com</a><br />license: MIT<br />tags:</li>



<li>spec-analysis</li>



<li>speckit<br />install:<br />files:
<ul class="wp-block-list">
<li>src: addon/commands/<br />dest: commands/</li>



<li>src: addon/hooks/<br />dest: hooks/</li>



<li>src: addon/templates/<br />dest: templates/</li>



<li>src: addon/bin/<br />dest: bin/<br />hooks:</li>
</ul>
</li>



<li>name: after-plan<br />file: addon/hooks/after-plan.yml</li>
</ul>



<p class="wp-block-paragraph">Explanation:</p>



<ul class="wp-block-list">
<li>install.files tells the Spec Kit installer how to copy files into the runtime extension directory. Keep the paths relative to the extension archive root.</li>



<li>hooks are entry points to a hook descriptor file (next).</li>
</ul>



<p class="wp-block-paragraph"><strong>Step 4</strong> — slash command file (full example)<br />File: addon/commands/speckit.extn.myfeature.md</p>



<p class="wp-block-paragraph">Description: Analyze the given Spec file for structural issues and return a JSON report with &#8220;summary&#8221; and &#8220;issues&#8221;.<br />Inputs:</p>



<ul class="wp-block-list">
<li>spec-path: path to the spec file. Defaults to the current spec in the workspace if omitted.</li>



<li>format: &#8220;json&#8221; (default) or &#8220;text&#8221;.</li>



<li>output: optional path to write the JSON report.</li>
</ul>



<p class="wp-block-paragraph">Outputs:</p>



<ul class="wp-block-list">
<li>result (JSON):<br />{<br />&#8220;summary&#8221;: &#8220;string&#8221;,<br />&#8220;issues&#8221;: [<br />{<br />&#8220;line&#8221;: 12,<br />&#8220;severity&#8221;: &#8220;warning&#8221;,<br />&#8220;ruleId&#8221;: &#8220;SP-001&#8221;,<br />&#8220;message&#8221;: &#8220;Example issue message&#8221;<br />}<br />]<br />}</li>
</ul>



<p class="wp-block-paragraph">Examples:</p>



<ul class="wp-block-list">
<li>/speckit.extn.myfeature spec-path=specs/login.spec format=json</li>



<li>/speckit.extn.myfeature format=text</li>
</ul>



<p class="wp-block-paragraph">Behavior:</p>



<ul class="wp-block-list">
<li>When invoked, the agent will run the extension&#8217;s hook or run wrapper that calls the C# CLI with &#8211;spec . The CLI emits structured JSON to stdout. The agent should present JSON results or a text summary as requested.</li>
</ul>



<p class="wp-block-paragraph">Notes:</p>



<ul class="wp-block-list">
<li>Keep examples explicit and provide the JSON schema so integrators know how to parse the result.</li>
</ul>



<p class="wp-block-paragraph"><strong>Step 5</strong> — hook descriptor (full example)<br />File: addon/hooks/after-plan.yml</p>



<p class="wp-block-paragraph">hook: after-plan<br />run: ./bin/run.sh<br />args:</p>



<ul class="wp-block-list">
<li>&#8211;spec</li>



<li>&#8220;{{spec.path}}&#8221;</li>



<li>&#8211;output</li>



<li>&#8220;{{workspace}}/.specify/extensions/com.yourorg.myfeature/latest-report.json&#8221;<br />env:<br />LOG_LEVEL: info</li>
</ul>



<p class="wp-block-paragraph">Notes:</p>



<ul class="wp-block-list">
<li>run points to the wrapper script in addon/bin/ that selects the correct binary for the host OS. This simplifies cross-platform support.</li>



<li>args include placeholders. <strong>Confirm the exact placeholder names in your Spec Kit version</strong>; many installations support {{spec.path}} and {{workspace}} or similar fields.</li>
</ul>



<p class="wp-block-paragraph"><strong>Step 6</strong> — C# helper (full source example)<br />We’ll implement a small console app that performs simple analysis: counts characters and looks for TODO comments as &#8220;issues&#8221;. It’s intentionally simple but structured to be extended.</p>



<p class="wp-block-paragraph">Project: src/MyFeatureRunner/MyFeatureRunner.csproj Exe net8.0 enable enable</p>



<p class="wp-block-paragraph">Program.cs (src/MyFeatureRunner/Program.cs)</p>



<pre class="wp-block-code"><code>using System.Text.Json;
using System.Text.Json.Serialization;

public static class Program
{
  public static int Main(string&#91;] args)
  {
    var parsed = CliArgs.Parse(args);
    if (!parsed.IsValid)
    {
      Console.Error.WriteLine(parsed.ErrorMessage);
      return parsed.ExitCode;
    }</code></pre>



<pre class="wp-block-preformatted"> <code>   try<br />    {<br />        var report = Analyzer.AnalyzeFile(parsed.SpecPath);<br />        var options = new JsonSerializerOptions { WriteIndented = true };<br />        string json = JsonSerializer.Serialize(report, options);<br /><br />        if (!string.IsNullOrEmpty(parsed.Output))<br />        {<br />            File.WriteAllText(parsed.Output, json);<br />            Console.WriteLine($"Report written to {parsed.Output}");<br />        }<br />        else<br />        {<br />            Console.WriteLine(json);<br />        }<br /><br />        return 0;<br />    }<br />    catch (FileNotFoundException ex)<br />    {<br />        Console.Error.WriteLine(ex.Message);<br />        return 3;<br />    }<br />    catch (Exception ex)<br />    {<br />        Console.Error.WriteLine($"Internal error: {ex.Message}");<br />        return 10;<br />    }<br />}<br /></code>}</pre>



<p class="wp-block-paragraph">CliArgs helper (embedded in same file or separate file for clarity):</p>



<pre class="wp-block-code"><code>internal sealed class CliArgs
{
public string SpecPath { get; init; } = string.Empty;
public string Output { get; init; } = string.Empty;
public bool IsValid { get; init; }
public string ErrorMessage { get; init; } = string.Empty;
public int ExitCode { get; init; }
public static CliArgs Parse(string&#91;] args)
{
    if (args.Length == 0)
        return new CliArgs { IsValid = false, ErrorMessage = "Usage: MyFeatureRunner --spec &lt;path&gt; &#91;--output &lt;path&gt;]", ExitCode = 2 };

    var dict = new Dictionary&lt;string, string&gt;(StringComparer.OrdinalIgnoreCase);
    for (int i = 0; i &lt; args.Length; i++)
    {
        if (args&#91;i].StartsWith("--") &amp;&amp; i + 1 &lt; args.Length)
        {
            dict&#91;args&#91;i]] = args&#91;i + 1];
            i++;
        }
    }

    if (!dict.TryGetValue("--spec", out var spec) || string.IsNullOrWhiteSpace(spec))
        return new CliArgs { IsValid = false, ErrorMessage = "Missing required --spec &lt;path&gt;", ExitCode = 2 };

    dict.TryGetValue("--output", out var output);

    return new CliArgs { IsValid = true, SpecPath = spec!, Output = output ?? string.Empty };
}
}
</code></pre>



<p class="wp-block-paragraph">Analyzer (src/MyFeatureRunner/Analyzer.cs)</p>



<p class="wp-block-paragraph"></p>



<pre class="wp-block-preformatted">using System.Text.Json.Serialization;<br /><br />public static class Analyzer<br />{<br />  public static AnalysisReport AnalyzeFile(string path)<br />  {<br />  if (!File.Exists(path))<br />    throw new FileNotFoundException($"Spec not found: {path}", path);<code> </code><br />   <code>var text = File.ReadAllText(path);<br />    var lines = text.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);<br /><br />    var issues = new List&lt;Issue&gt;();<br />    for (int i = 0; i &lt; lines.Length; i++)<br />    {<br />        var line = lines[i];<br />        if (line.Contains("TODO", StringComparison.OrdinalIgnoreCase))<br />        {<br />            issues.Add(new Issue<br />            {<br />                Line = i + 1,<br />                Severity = "warning",<br />                RuleId = "SP-001",<br />                Message = "Found TODO in spec; consider clarifying acceptance criteria."<br />            });<br />        }<br />    }<br /><br />    return new AnalysisReport<br />    {<br />        Summary = $"Spec has {text.Length} chars, {lines.Length} non-empty lines, {issues.Count} issues.",<br />        Issues = issues<br />    };<br />}<br /></code>}</pre>



<pre class="wp-block-code"><code>public class AnalysisReport
{
&#91;JsonPropertyName("summary")]
public string Summary { get; set; } = string.Empty;
&#91;JsonPropertyName("issues")]
public List&lt;Issue&gt; Issues { get; set; } = new();
}
</code></pre>



<pre class="wp-block-code"><code>public class Issue
{
&#91;JsonPropertyName("line")]
public int Line { get; set; }
&#91;JsonPropertyName("severity")]
public string Severity { get; set; } = string.Empty;
&#91;JsonPropertyName("ruleId")]
public string RuleId { get; set; } = string.Empty;
&#91;JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;
}</code></pre>



<p class="wp-block-paragraph">Design notes:</p>



<ul class="wp-block-list">
<li>Analyzer.AnalyzeFile is pure and easily testable. Keep the CLI thin.</li>



<li>Exit codes:
<ul class="wp-block-list">
<li>0 success</li>



<li>2 argument error / usage</li>



<li>3 not found (spec path)</li>



<li>10 internal error</li>
</ul>
</li>
</ul>



<p class="wp-block-paragraph"><strong>Step 7</strong> — unit tests (xUnit)<br />Create src/MyFeatureRunner.Tests/MyFeatureRunner.Tests.csproj and a simple test that calls Analyzer directly (no process spawning), ensuring repeatable logic.</p>



<p class="wp-block-paragraph">Sample test (src/MyFeatureRunner.Tests/AnalyzerTests.cs)</p>



<pre class="wp-block-code"><code>using Xunit;
using System.IO;
using System.Text.Json;

public class AnalyzerTests
{
  &#91;Fact]
  public void AnalyzeFile_ReturnsIssuesForTODO()
  {
    var tmp = Path.GetTempFileName();
    File.WriteAllText(tmp, "Line one\nTODO: add acceptance\nAnother line\n");
    var report = Analyzer.AnalyzeFile(tmp);
    Assert.Contains("Spec has", report.Summary);
    Assert.Single(report.Issues);
    Assert.Equal("SP-001", report.Issues&#91;0].RuleId);
  }
}</code></pre>



<p class="wp-block-paragraph"><strong>Step 8</strong> — run wrappers and permissions<br />Create addon/bin/run.sh (Unix wrapper):</p>



<pre class="wp-block-code"><code>#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" &amp;&amp; pwd)"
PLATFORM_BIN=""
if &#91;&#91; "$(uname -s)" == "Linux" ]]; then
PLATFORM_BIN="$ROOT/linux/MyFeatureRunner"
elif &#91;&#91; "$(uname -s)" == "Darwin" ]]; then
PLATFORM_BIN="$ROOT/macos/MyFeatureRunner"
else
PLATFORM_BIN="$ROOT/windows/MyFeatureRunner.exe"
fi
exec "$PLATFORM_BIN" "$@"</code></pre>



<p class="wp-block-paragraph">Make it executable: chmod +x addon/bin/run.sh.</p>



<p class="wp-block-paragraph">Windows wrapper addon/bin/run.ps1 (PowerShell):</p>



<pre class="wp-block-code"><code>param(&#91;Parameter(ValueFromRemainingArguments=$true)]$Args)
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
$os = (Get-CimInstance -ClassName Win32_OperatingSystem).Caption
$exe = Join-Path $root "windows\MyFeatureRunner.exe"
&amp; $exe @Args</code></pre>



<p class="wp-block-paragraph"><strong>Step 9</strong> — installer scripts<br /></p>



<pre class="wp-block-code"><code>install.sh:
#!/usr/bin/env bash
set -euo pipefail
EXT_ID="com.yourorg.myfeature"
DEST=".specify/extensions/$EXT_ID"
mkdir -p "$DEST"
cp -r addon "$DEST/"
cp extension.yml "$DEST/"
cp README.md "$DEST/"
echo "Installed extension to $DEST"

install.ps1 (PowerShell):
param(&#91;string]$Dest = ".specify/extensions/com.yourorg.myfeature")
New-Item -ItemType Directory -Force -Path $Dest | Out-Null
Copy-Item -Path "addon*" -Destination $Dest -Recurse -Force
Copy-Item -Path "extension.yml" -Destination $Dest -Force
Copy-Item -Path "README.md" -Destination $Dest -Force
Write-Host "Installed extension to $Dest"</code></pre>



<p class="wp-block-paragraph"><strong>Step 10</strong> — test locally (step-by-step)</p>



<ol class="wp-block-list">
<li>In a Spec Kit project:<br />mkdir -p .specify/extensions<br />cp -r com.yourorg.myfeature .specify/extensions/</li>



<li>Confirm files:<br />ls .specify/extensions/com.yourorg.myfeature</li>



<li>Run the analyzer directly:<br />dotnet run &#8211;project src/MyFeatureRunner &#8212; &#8211;spec examples/sample-spec.md Expected JSON printed or &#8220;Report written to &#8221; if &#8211;output provided.</li>



<li>Trigger the after-plan hook via your normal Spec Kit flow (specify → plan) and confirm:
<ul class="wp-block-list">
<li>Hook runs and writes latest-report.json in the extension folder (or check STDOUT in logs).</li>



<li>If hook failed, inspect agent logs or the extension&#8217;s stderr captured by the CLI.</li>
</ul>
</li>
</ol>



<p class="wp-block-paragraph"><strong>Step 11</strong> — publishing &amp; packaging with CI (GitHub Actions)<br />A sample workflow .github/workflows/publish.yml that:</p>



<ul class="wp-block-list">
<li>Builds and publishes single-file binaries for multiple runtimes</li>



<li>Packages the extension folder into a zip</li>



<li>Uploads the zip as an artifact (or creates a release)</li>
</ul>



<p class="wp-block-paragraph">Sample YAML (abridged for clarity):</p>



<pre class="wp-block-code"><code>name: Build and Package Extension
on:
push:
tags:
- 'v*.<em>.</em>'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
runtime: &#91;linux-x64, linux-arm64, osx-arm64, win-x64]
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: Publish ${{ matrix.runtime }}
run: |
dotnet publish src/MyFeatureRunner -c Release -r ${{ matrix.runtime }}
-p:PublishSingleFile=true -p:PublishTrimmed=true -o ./addon/bin/${{ matrix.runtime }}
- name: Organize binaries
run: |
# move to the platform folder expected by the extension, e.g. linux-&gt;linux, osx-&gt;macos, win-&gt;windows
mkdir -p addon/bin/linux addon/bin/macos addon/bin/windows
if &#91;&#91; "${{ matrix.runtime }}" == linux* ]]; then mv ./addon/bin/${{ matrix.runtime }}/MyFeatureRunner ./addon/bin/linux/MyFeatureRunner; fi
if &#91;&#91; "${{ matrix.runtime }}" == osx* ]]; then mv ./addon/bin/${{ matrix.runtime }}/MyFeatureRunner ./addon/bin/macos/MyFeatureRunner; fi
if &#91;&#91; "${{ matrix.runtime }}" == win* ]]; then mv ./addon/bin/${{ matrix.runtime }}/MyFeatureRunner.exe ./addon/bin/windows/MyFeatureRunner.exe; fi
- name: Zip extension
if: ${{ matrix.runtime == 'linux-x64' }}
run: |
zip -r com.yourorg.myfeature-${{ github.ref_name }}.zip extension.yml addon README.md
- name: Upload package
uses: actions/upload-artifact@v4
with:
name: extension-zip
path: com.yourorg.myfeature-${{ github.ref_name }}.zip</code></pre>



<p class="wp-block-paragraph">Notes:</p>



<ul class="wp-block-list">
<li>This workflow demonstrates building multiple runtimes and packaging into a zip during the linux-x64 job; you can centralize packaging in a separate job that depends on build artifacts.</li>



<li>For production, consider signing artifacts and publishing to GitHub Releases or a private artifact store.</li>
</ul>



<p class="wp-block-paragraph"><strong>Step 12</strong> — catalog entry &amp; checksum<br />Create a catalog JSON entry referencing the ZIP URL and a checksum.</p>



<p class="wp-block-paragraph">Example catalog entry:</p>



<pre class="wp-block-code"><code>{
"id": "com.yourorg.myfeature",
"name": "My Feature",
"version": "0.1.0",
"url": "<a href="https://example.com/com.yourorg.myfeature-0.1.0.zip">https://example.com/com.yourorg.myfeature-0.1.0.zip</a>",
"checksum": "sha256:abcdef123456..."
}</code></pre>



<p class="wp-block-paragraph">Generate a checksum:</p>



<ul class="wp-block-list">
<li>Unix: sha256sum com.yourorg.myfeature-0.1.0.zip | awk &#8216;{print $1}&#8217;</li>



<li>PowerShell: (Get-FileHash .\com.yourorg.myfeature-0.1.0.zip -Algorithm SHA256).Hash</li>
</ul>



<p class="wp-block-paragraph"><strong>Step 13</strong> — troubleshooting and debugging checklist</p>



<ul class="wp-block-list">
<li>Hook not executed:
<ul class="wp-block-list">
<li>Confirm extension.yml lists the hook and was installed to .specify/extensions//.</li>



<li>Confirm hook file path is correct and marked executable if it’s a script.</li>
</ul>
</li>



<li>Binary fails on host:
<ul class="wp-block-list">
<li>Ensure correct platform binary published or require dotnet runtime and use wrapper scripts to run dotnet MyFeatureRunner.dll.</li>



<li>Check file permissions: chmod +x on Unix binaries.</li>
</ul>
</li>



<li>Placeholder expansion:
<ul class="wp-block-list">
<li>Verify your Spec Kit installation supports placeholders like {{spec.path}}; consult your Spec Kit version docs and update hook args.</li>
</ul>
</li>



<li>Inspect logs:
<ul class="wp-block-list">
<li>Check Spec Kit or agent logs; hooks often have stdout/stderr captured — look for error messages and exit codes.</li>
</ul>
</li>



<li>Test locally:
<ul class="wp-block-list">
<li>Run the wrapper or the CLI directly with the same args the hook would send to reproduce failures.</li>
</ul>
</li>
</ul>



<p class="wp-block-paragraph">Security &amp; governance checklist</p>



<ul class="wp-block-list">
<li>Do not embed secrets inside the extension. Use environment variables or configuration files that the consumer supplies.</li>



<li>Document any network calls or telemetry the extension performs.</li>



<li>Prefer offline, deterministic tools; minimize external dependencies.</li>



<li>Provide checksums and consider signing releases for consumers to verify integrity.</li>



<li>Encourage consumers to review source before installing; keep the extension small and source-available.</li>
</ul>



<p class="wp-block-paragraph">Best practices &amp; gotchas</p>



<ul class="wp-block-list">
<li>Use JSON for structured output; it’s easier for agents and hooks to parse than free text.</li>



<li>Keep the CLI deterministic and non-interactive. Hooks execute noninteractively — no prompts.</li>



<li>If you can’t ship native executables for all platforms, ship a single DLL and small launcher scripts that call dotnet MyRunner.dll; document runtime requirements.</li>



<li>Consider a self-test command or endpoint (e.g., /speckit.extn.myfeature.selftest) that the user can run after install to verify everything works.</li>
</ul>



<p class="wp-block-paragraph"><strong>Complete end-to-end checklist (before publishing)</strong></p>



<ul class="wp-block-list">
<li>extension.yml exists and maps files correctly.</li>



<li>addon/commands contains command files with examples and output schema.</li>



<li>addon/hooks reference run wrappers and include correct placeholders.</li>



<li>addon/bin contains published artifacts for supported OSes or a DLL + run wrappers.</li>



<li>README documents requirements, install, usage, and troubleshooting.</li>



<li>install scripts work for Unix and Windows for local testing.</li>



<li>Example spec exists and tests validate analyzer logic.</li>



<li>CI builds and packages artifacts and uploads ZIP to release/host.</li>



<li>Catalog entry (if publishing) includes correct URL and checksum.</li>
</ul>



<p class="wp-block-paragraph">Note: The initial draft of this post (and all the code) was written by <a href="https://github.com/JesseLiberty/BlogWriter/blob/main/README.md">BlogWriter</a> and then<br />edited by Jesse Liberty. Illustrations by Copilot. <strong>Caution</strong>: LLMs make mistakes; this post is offered as is.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Creating Spec Kit Presets Step-By-Step</title>
		<link>https://jesseliberty.com/2026/10/09/creating-spec-kit-presets-step-by-step/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Fri, 09 Oct 2026 12:33:06 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13851</guid>

					<description><![CDATA[This guide assumes you already understand what Spec Kit and presets are and focuses on the who/what/where/how of authoring, testing, and publishing a preset. The instructions are concrete and practical — including a minimal manifest example, the recommended folder layout, &#8230; <a href="https://jesseliberty.com/2026/10/09/creating-spec-kit-presets-step-by-step/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">This guide assumes you already understand what Spec Kit and presets are and focuses on the who/what/where/how of authoring, testing, and publishing a preset. The instructions are concrete and practical — including a minimal manifest example, the recommended folder layout, local test workflow, a tiny C# validation snippet, and packaging/publishing notes.</p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="800" height="796" src="https://jesseliberty.com/wp-content/uploads/2026/10/preset-wizrd-2-800x796.jpg" alt="" class="wp-image-13853" style="aspect-ratio:1.0050158209305051;width:400px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/10/preset-wizrd-2-800x796.jpg 800w, https://jesseliberty.com/wp-content/uploads/2026/10/preset-wizrd-2-150x149.jpg 150w, https://jesseliberty.com/wp-content/uploads/2026/10/preset-wizrd-2-300x298.jpg 300w, https://jesseliberty.com/wp-content/uploads/2026/10/preset-wizrd-2-768x764.jpg 768w, https://jesseliberty.com/wp-content/uploads/2026/10/preset-wizrd-2.jpg 971w" sizes="auto, (max-width: 800px) 100vw, 800px" /></figure>



<span id="more-13851"></span>



<ul class="wp-block-list">
<li>A preset is a versioned package that supplies template files, CLI prompts/companion prompt text, small scripts, and tiny content changes (terminology, defaults, localization). Presets are layered at runtime; when Spec Kit resolves a template or prompt, it walks the stack, and the highest-priority “winning” file is used.</li>



<li>Presets are intended for content/style/localization changes. Avoid heavy runtime logic—use Bundles or Extensions if you need stateful automation.</li>
</ul>



<p class="wp-block-paragraph"><strong>Step </strong>1—design: decide scope, naming, and conventions</p>



<ul class="wp-block-list">
<li>Pick exactly what you will override:
<ul class="wp-block-list">
<li>templates/—spec, plan, or task templates (Markdown).</li>



<li><strong>commands</strong>/—CLI command prompt text or companion .prompt.md files visible to users.</li>



<li>scripts/—shell/PowerShell scripts used by templates or lifecycle hooks.</li>



<li><strong>small localized wording changes, default values, or doc snippets.</strong></li>
</ul>
</li>



<li>Keep presets focused. <em>One preset should do one job</em> (branding, Spanish localization, trimmed plan structure, etc.).</li>



<li>Choose an ID, version, and a default priority. Example: id = com.example.my-company-style, version = 0.1.0, priority = 8. Lower priority number = higher precedence (default is 10).</li>



<li>Record author name/email and a short description in the manifest. These fields help consumers and catalogs list and audit presets.</li>
</ul>



<p class="wp-block-paragraph"><strong>Step </strong>2—create the folder structure and the manifest</p>



<ul class="wp-block-list">
<li>Create a folder for the preset. Example:<br />my-company-style/</li>



<li>At the root, create a manifest file named preset.yml and add subfolders for overrides:
<ul class="wp-block-list">
<li>templates/</li>



<li>commands/</li>



<li>scripts/</li>



<li>README.md</li>
</ul>
</li>
</ul>



<p class="wp-block-paragraph"><strong>Minimal example</strong> preset.yml (place at my-company-style/preset.yml)<br />name: my-company-style<br />id: com.example.my-company-style<br />version: 0.1.0<br />description: &#8220;Company wording, trimmed plan structure, and Spanish localization.&#8221;<br />author:<br />name: Example Corp<br />email: <a href="mailto:infra@example.com">infra@example.com</a><br />priority: 8<br />tags:</p>



<ul class="wp-block-list">
<li>style</li>



<li>localization</li>
</ul>



<p class="wp-block-paragraph"><strong>Notes about the manifest</strong>:</p>



<ul class="wp-block-list">
<li>Spec Kit reads preset.yml to register the preset.</li>



<li>The important required fields are id, name, and version; add description, author, priority, and tags for usability.</li>



<li>You can extend the manifest with catalog metadata if you plan to publish.</li>
</ul>



<p class="wp-block-paragraph"><strong>Step 3</strong>—place overrides in the correct locations</p>



<ul class="wp-block-list">
<li>templates/: copy the exact core template filename you want to override (for example, plan-template.md) and edit it. The runtime resolution uses filenames/paths to match.</li>



<li>commands/: put files that replace command prompt text or companion prompts. For example, speckit.constitution.md or .prompt.md files.</li>



<li>scripts/: put helper scripts referenced by templates or lifecycle hooks.</li>



<li>README.md: explain what the preset does, installation implications, and any migration guidance.</li>
</ul>



<p class="wp-block-paragraph"><strong>Important</strong>: keep the same filenames and relative paths used by the core Spec Kit repository (you can inspect core templates in the Spec Kit repo to find correct names). During installation, preset files are copied to .specify/presets// and spec resolution walks the stack automatically, so you don’t manually merge templates.</p>



<p class="wp-block-paragraph"><strong>Example</strong>: override the plan template to add a C# code sample</p>



<ul class="wp-block-list">
<li>Create templates/plan-template.md and include a fenced C# block in the example section: &#8230;in the template&#8217;s code example area include: <br /></li>
</ul>



<pre class="wp-block-code"><code></code><code>// Example using our company logging convention </code>
<code>public class AccountService { private readonly ILogger _log; </code><code>public AccountService(ILogger log) => _log = log; }</code></pre>



<p class="wp-block-paragraph">(Templates are plain Markdown — using C# samples inside them is fine.)</p>



<p class="wp-block-paragraph"><strong>Step 4</strong> — local testing during development (fast feedback loop)</p>



<ul class="wp-block-list">
<li>Install your preset into a local project while developing:<br />specify preset add &#8211;dev /path/to/my-company-style</li>



<li>Run generation commands that exercise the templates or prompts you modified:<br />specify plan &#8211;input spec=&#8221;Add user login&#8221;</li>



<li>Inspect the generated output to verify your overrides took effect.</li>



<li>Use Spec Kit helper commands to debug resolution:<br />specify preset resolve<br />specify artifact (or whichever command your Spec Kit provides to inspect which file “wins”)</li>



<li>Iterate: change files in your local preset directory and re-run the generation command. The &#8211;dev install mode points to your local copy for quick iteration.</li>
</ul>



<p class="wp-block-paragraph"><strong>Step </strong>5—validate the manifest (quick programmatic check)</p>



<ul class="wp-block-list">
<li>A simple validation ensures required fields are present and helps catch YAML typos. Below is a tiny C# example using YamlDotNet that reads preset.yml and errors if id/name/version are missing.</li>
</ul>



<p class="wp-block-paragraph"><strong>Commands</strong>:</p>



<ul class="wp-block-list">
<li>Add the YamlDotNet package (run in your dotnet project directory):<br />dotnet add package YamlDotNet</li>
</ul>



<p class="wp-block-paragraph"><strong>C# Program</strong> (Program.cs):</p>



<pre class="wp-block-code"><code>using System;
using System.IO;
using System.Collections.Generic;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

var yaml = File.ReadAllText("preset.yml");
var deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
var preset = deserializer.Deserialize&lt;Dictionary&lt;string, object&gt;&gt;(yaml);

string&#91;] required = new&#91;] { "id", "name", "version" };
foreach (var r in required)
{
  if (!preset.ContainsKey(r))
  {
    Console.Error.WriteLine($"Missing required manifest field: {r}");
    Environment.Exit(2);
  }
}
Console.WriteLine($"Preset OK: {preset&#91;"name"]} v{preset&#91;"version"]}");</code></pre>



<ul class="wp-block-list">
<li>dotnet run (or dotnet build + dotnet run) in the project containing Program.cs and preset.yml.</li>



<li>Exit codes: 0 for OK, non-zero for missing required fields. Integrate this into CI to fail fast on manifest mistakes.</li>
</ul>



<p class="wp-block-paragraph"><strong>Step </strong>6—tests and CI</p>



<ul class="wp-block-list">
<li>Unit or integration tests: create tests that run specify commands in a controlled workspace (temp directory), generate artifacts, and assert expected strings or structural pieces exist.</li>



<li>Snapshot testing: store small canonical generated files in tests and compare outputs after generation.</li>



<li>Linting: run your YAML validator and any custom checks (e.g., disallow certain words, ensure semantic versioning).</li>



<li>CI: run the validation and generation tests for pull requests. If your preset includes scripts, run them in CI to ensure cross-platform behavior or provide both shell and PowerShell variants.</li>
</ul>



<p class="wp-block-paragraph"><strong>Step </strong>7—package and publish</p>



<ul class="wp-block-list">
<li>Options for sharing:
<ul class="wp-block-list">
<li>Publish the preset repository on GitHub/GitLab and reference it from a catalog.</li>



<li>Host a catalog.json or catalog.yaml listing your preset metadata and URL. Consumers can add presets from catalogs.</li>



<li>Use community tooling (e.g., presetify) to generate catalog entries and help with validation/publishing to community catalogs.</li>
</ul>
</li>



<li>For internal distribution, your team can host the preset repo or a catalog on an internal server and point projects at it.</li>



<li>Ensure you tag releases using semantic versioning so consumers can pin versions: e.g., v0.1.0.</li>
</ul>



<p class="wp-block-paragraph"><strong>Step 8</strong>—installation and lifecycle for consumers</p>



<ul class="wp-block-list">
<li>Install from a catalog or URL:<br />specify preset add<br />specify preset add &#8211;version<br />specify preset add &#8211;from</li>



<li>Install locally for development:<br />specify preset add &#8211;dev &lt;path/to/preset&gt;</li>



<li>Remove:<br />specify preset remove</li>



<li>Upgrading a preset is a matter of updating the version and having consumers run an upgrade or specify preset add with a version.</li>
</ul>



<p class="wp-block-paragraph"><strong>Best practices &amp; warnings</strong></p>



<ul class="wp-block-list">
<li>Keep presets limited to content/formatting/localization. Don’t use presets to implement complex runtime automation.</li>



<li>Follow semantic versioning. Document changes in README and changelog.</li>



<li>Prefer small, focused presets over large monolithic presets; it’s easier to compose and upgrade.</li>



<li>Security: don’t blindly trust third-party catalogs. Inspect preset contents before install and ensure your project policy permits external presets.</li>



<li>Avoid breaking template API contracts. If you change placeholders or required variables in a template, document the change and bump major version.</li>



<li>Use meaningful tags in preset.yml so consumers (and catalog tools) can filter presets: tags: [style, spanish, onboarding]</li>



<li>Provide README.md with examples: “How this preset changes plan-template.md” and sample generated output.</li>



<li>If you want to supply per-command prompts, include companion .prompt.md files under commands/ with clear naming.</li>
</ul>



<p class="wp-block-paragraph">Note: The initial draft of this post was written by <a href="https://github.com/JesseLiberty/BlogWriter/blob/main/README.md">BlogWriter</a> and then<br />edited by Jesse Liberty.  Illustrations by Copilot. <strong>Caution</strong>: LLMs make mistakes; this post is offered as is.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>4 Solutions to Spec Kit Issues</title>
		<link>https://jesseliberty.com/2026/10/08/4-solutions-to-spec-kit-issues/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Thu, 08 Oct 2026 19:33:32 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13847</guid>

					<description><![CDATA[Your design docs look different across teams. Some repos forget the QA gate. Integrations (Jira, CI) are half-baked and duplicated as bash scripts. Onboarding a new engineer requires five manual steps to wire the same set of templates, commands, and &#8230; <a href="https://jesseliberty.com/2026/10/08/4-solutions-to-spec-kit-issues/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Your design docs look different across teams. Some repos forget the QA gate. Integrations (Jira, CI) are half-baked and duplicated as bash scripts. Onboarding a new engineer requires five manual steps to wire the same set of templates, commands, and pipelines.</p>



<p class="wp-block-paragraph">Spec Kit exposes four primitives that solve these problems in a structured, maintainable way:</p>



<ul class="wp-block-list">
<li>Presets fix content and wording (make docs consistent).</li>



<li>Extensions add behavior and integrations (run code, call Jira/CI).</li>



<li>Workflows orchestrate multi-step, resumable pipelines (add gates and state).</li>



<li>Bundles package and version a curated set of the above (one-command onboarding).</li>
</ul>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="800" height="797" src="https://jesseliberty.com/wp-content/uploads/2026/10/image-800x797.png" alt="" class="wp-image-13848" style="aspect-ratio:1.0037689030385317;width:401px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/10/image-800x797.png 800w, https://jesseliberty.com/wp-content/uploads/2026/10/image-150x150.png 150w, https://jesseliberty.com/wp-content/uploads/2026/10/image-300x299.png 300w, https://jesseliberty.com/wp-content/uploads/2026/10/image-767x765.png 767w, https://jesseliberty.com/wp-content/uploads/2026/10/image.png 930w" sizes="auto, (max-width: 800px) 100vw, 800px" /></figure>



<p class="wp-block-paragraph">This post explains each primitive plainly, shows concrete manifests and examples (CLI + Visual Studio Code), and gives authoring, troubleshooting, and operational guidance so you can pick the right tool for the job.</p>



<span id="more-13847"></span>



<p class="wp-block-paragraph"><strong>At-a-glance summary</strong></p>



<ul class="wp-block-list">
<li><strong>Preset </strong>— content &amp; prompt overrides (templates/command text). Use to change wording, templates, or organization-wide conventions without changing core code.</li>



<li><strong>Extension </strong>— new capabilities (commands, integrations, hooks). Use to add Jira, git automation, quality gates, or domain-specific behaviors.</li>



<li><strong>Workflow</strong> — a resumable, multi-step YAML pipeline that orchestrates Spec Kit commands, shell steps and human gates.</li>



<li><strong>Bundle </strong>— a versioned, shareable package that installs a curated set of presets, extensions, workflows, and steps together for a team/role.</li>
</ul>



<p class="wp-block-paragraph"><strong>What they change and when to pick them</strong></p>



<ul class="wp-block-list">
<li>Need different wording, templates, or company style? → Preset.</li>



<li>Need new integrations, commands, or hooks that run code? → Extension.</li>



<li>Need to automate a repeatable multi-step process (with gates and state)? → Workflow.</li>



<li>Need to ship a set of the above to teammates as one installable artifact? → Bundle.</li>
</ul>



<p class="wp-block-paragraph"><strong>Deep dive: Presets</strong> — change what the agent writes<br />What a preset does</p>



<ul class="wp-block-list">
<li>Presets override templates and prompt text used by Spec Kit’s agent. They are focused on content: wording, section structure, localization, default fields and the text the CLI uses when creating specs/plans/tasks.</li>



<li>Presets do not (normally) run code, call external services, or change engine behavior.</li>
</ul>



<p class="wp-block-paragraph"><strong>When to use a preset</strong></p>



<ul class="wp-block-list">
<li>Enforce company style, legal or compliance wording.</li>



<li>Provide localized templates (Spanish, Japanese).</li>



<li>Change default structure (one-pager vs full SDD).</li>



<li>Adjust agent prompt wording for tone/conciseness.</li>
</ul>



<p class="wp-block-paragraph">Concrete preset manifest (example)<br />File path: .specify/presets/company-style.yaml</p>



<pre class="wp-block-code"><code>priority: 10
name: company-style
description: "Company legal &amp; style overrides; Spanish localization for Latin America teams"

templates:
  spec.summary: "Propósito corto (1–2 oraciones)"
  spec.description: |
    Descripción:
    - Objetivo: {{ inputs.goal }}
    - Stakeholders: {{ inputs.stakeholders | join(', ') }}
  spec.security: |
    Consideraciones de seguridad:
    - Autenticación: se requiere 2FA para nuevos endpoints
    - Datos sensibles: cifrado en tránsito y en reposo</code></pre>



<p class="wp-block-paragraph"><strong>How presets stack</strong></p>



<ul class="wp-block-list">
<li>Multiple presets can be active. They stack by numeric priority: lower number = higher precedence. A global company preset (priority 10) can be overridden by a repo-level preset with priority 5.</li>



<li>When a template ID is present in multiple presets, the lowest-numbered preset wins for that template only.</li>
</ul>



<p class="wp-block-paragraph"><strong>Authoring and publishing a preset — step-by-step</strong></p>



<ol class="wp-block-list">
<li>Create a local preset file in .specify/presets/ (YAML or JSON).</li>



<li>Test locally by running a generate command that would use the template. Example:
<ul class="wp-block-list">
<li>CLI: specify plan &#8211;input spec=&#8221;Add login&#8221;</li>



<li>Check the generated text for your new wording.</li>
</ul>
</li>



<li>If satisfied, publish the preset to your Spec Kit catalog (your org’s registry) so teams can add it with specify preset add <a href="mailto:company-style@1.0.0">company-style@1.0.0</a>.</li>



<li>When updates are needed, bump the manifest version and publish a new release. Consumers can upgrade with specify preset update company-style.</li>
</ol>



<p class="wp-block-paragraph"><strong>VS Code: how to install and test a preset</strong></p>



<ul class="wp-block-list">
<li>Terminal:
<ol class="wp-block-list">
<li>Open your workspace in VS Code.</li>



<li>Open integrated terminal (Ctrl+`).</li>



<li>Run: specify preset add company-style</li>
</ol>
</li>



<li>UI (Spec Kit Companion):
<ol class="wp-block-list">
<li>Open the Spec Kit sidebar.</li>



<li>Select Presets → Search → company-style → Install.</li>



<li>Use the “Preview” or “Generate” action in the sidebar to see the template applied.</li>
</ol>
</li>
</ul>



<p class="wp-block-paragraph"><strong>Troubleshooting common preset issues</strong></p>



<ul class="wp-block-list">
<li>Preset not taking effect: check priorities (lower wins), ensure the preset ID and version installed, and confirm the template ID you modified matches the agent’s template ID (typos happen).</li>



<li>Conflicting overrides: list installed presets and remove/adjust the one you don’t want (specify preset list; specify preset remove ).</li>
</ul>



<p class="wp-block-paragraph"><strong>Deep dive: Extensions</strong> — add behavior and integrations<br />What an extension does</p>



<ul class="wp-block-list">
<li>Extensions are plugins that can register new CLI commands, provide templates, offer hooks, and run code. They can call external services (Jira, test runners), enforce checks, or modify repository state (branch creation, commits).</li>



<li>Use extensions when you need logic beyond textual overrides.</li>
</ul>



<p class="wp-block-paragraph">Example extension manifest (minimal)<br />File: extension.yaml</p>



<pre class="wp-block-code"><code>id: git-jira
version: 0.1.0
name: "Git + Jira integration"
description: "Creates branches, creates Jira tickets, and wires plan acceptance to ticket creation."

commands:
  - id: git.create-branch
    command: specify git branch-for-spec
  - id: jira.create-issue
    command: specify jira create-issue --input summary="{{ inputs.summary }}"

hooks:
  onPlanAccepted: jira.create-issue

permissions:
  network:
    - "jira.example.com"
filesystem:
  read:
    - ".git"
  write:
    - ".git/refs/heads"</code></pre>



<p class="wp-block-paragraph"><strong>Authoring an extension — quick guide</strong></p>



<ol class="wp-block-list">
<li>Scaffold an extension (your preferred runtime: Node, shell scripts, or compiled binary). Keep it small and well-scoped.</li>



<li>Register commands in the manifest and map them to your executable or script.</li>



<li>Implement and mock external calls for local testing.</li>



<li>Test in a local repo:
<ul class="wp-block-list">
<li>Install locally: specify extension add ./path/to/extension</li>



<li>Run the new command: specify git create-branch &#8211;input spec=&#8221;&#8230;&#8221;</li>
</ul>
</li>



<li>Publish to catalog once reviewed.</li>
</ol>



<p class="wp-block-paragraph">Example behavior: create branch + Jira ticket when a plan is accepted</p>



<ul class="wp-block-list">
<li>Extension registers a hook onPlanAccepted.</li>



<li>When a workflow step marks the plan accepted, the extension is invoked to create a Jira ticket and a branch.</li>



<li>Because extensions can run code or call networks, follow security practices (see Security section).</li>
</ul>



<p class="wp-block-paragraph"><strong>VS Code: installing and using an extension</strong></p>



<ul class="wp-block-list">
<li>Terminal:
<ol class="wp-block-list">
<li>Open integrated terminal (Ctrl+`).</li>



<li>Run: specify extension add git-jira</li>
</ol>
</li>



<li>UI (Spec Kit Companion):
<ol class="wp-block-list">
<li>Presets/Extensions view → Search “git-jira” → Install.</li>



<li>The extension’s commands will show up in the Command Palette (Ctrl+Shift+P) under Spec Kit commands; or the companion UI exposes buttons like “Create Jira issue”.</li>
</ol>
</li>
</ul>



<p class="wp-block-paragraph">Troubleshooting extensions</p>



<ul class="wp-block-list">
<li>Network permissions denied: check the extension manifest’s network permission and your environment firewall.</li>



<li>Command missing after install: ensure the extension installed successfully and restart the terminal/VS Code if necessary.</li>
</ul>



<p class="wp-block-paragraph"><strong>Deep dive: Workflows </strong>— orchestrate multi-step, resumable pipelines<br />What a workflow does</p>



<ul class="wp-block-list">
<li>A workflow is a YAML pipeline that composes agent steps, shell steps, and human gates. Workflows persist run state so you can pause for review and resume later.</li>



<li>Use workflows to enforce reproducible processes: assess → plan → implement → test → sign-off.</li>
</ul>



<p class="wp-block-paragraph">Expanded example workflow (detailed)<br />File: sdd-pipeline.yml</p>



<pre class="wp-block-code"><code>name: sdd-pipeline
inputs:
  spec:
    type: string
    required: true

steps:
  - id: assess
    type: agent
    command: specify assess --input spec="{{ inputs.spec }}"
    outputs:
      summary: string

  - id: plan
    type: agent
    command: specify plan --input spec="{{ steps.assess.summary }}"
    outputs:
      branch: string
      plan_url: string

  - id: create-branch
    type: shell
    command: |
      git checkout -b "{{ steps.plan.branch }}"
      git push -u origin "{{ steps.plan.branch }}"
    continueOnError: false

  - id: run-tests
    type: shell
    command: |
      ./run-tests.sh --report results.json
    outputs:
      test_report: results.json
    continueOnError: false

  - id: review
    type: gate
    approvers:
      - "team-lead@example.com"
    timeout: "72h"</code></pre>



<p class="wp-block-paragraph"><strong>Key workflow features demonstrated</strong></p>



<ul class="wp-block-list">
<li>Inputs and typed outputs — later steps can reference earlier outputs (templating with {{ }}).</li>



<li>Mix of agent and shell steps.</li>



<li>Human gate with approvers and timeout.</li>



<li>Persisted run state to allow resumption.</li>
</ul>



<p class="wp-block-paragraph">Run workflows — CLI and VS Code</p>



<ul class="wp-block-list">
<li>Install catalog workflow:<br />specify workflow add sdd-pipeline</li>



<li>Run from catalog:<br />specify workflow run sdd-pipeline &#8211;input spec=&#8221;Add user authentication&#8221;</li>



<li>Run a local workflow YAML:<br />specify workflow run ./sdd-pipeline.yml &#8211;input spec=&#8221;Add payment flow&#8221;</li>
</ul>



<p class="wp-block-paragraph">VS Code:</p>



<ul class="wp-block-list">
<li>Terminal: run the same specify workflow commands.</li>



<li>UI (Spec Kit Companion):
<ol class="wp-block-list">
<li>Open Workflows in the sidebar.</li>



<li>Choose a catalog workflow or point to a local YAML.</li>



<li>Click “Start run” and follow the run UI to see step outputs and approve gates.</li>
</ol>
</li>
</ul>



<p class="wp-block-paragraph"><strong>Authoring a workflow</strong> — best practices</p>



<ul class="wp-block-list">
<li>Validate inputs: declare required inputs and types.</li>



<li>Make steps idempotent where possible (re-running a branch creation should be safe or fail cleanly).</li>



<li>Persist minimal but useful outputs for later steps (e.g., branch name, plan URL).</li>



<li>Add clear error handling: use continueOnError judiciously and provide logs for shell steps.</li>



<li>Test locally by running the workflow with small inputs and stepping through the UI or CLI.</li>
</ul>



<p class="wp-block-paragraph"><strong>Troubleshooting workflows</strong></p>



<ul class="wp-block-list">
<li>Workflow fails at shell step: open the run’s step logs, reproduce the commands locally in the terminal, fix failing command or environment.</li>



<li>Gate never approved: ensure approvers are reachable emails and the companion UI sends notifications; approvers can resume runs from VS Code or CLI.</li>
</ul>



<p class="wp-block-paragraph"><strong>Deep dive: Bundles</strong> — package and share a curated stack<br />What a bundle does</p>



<ul class="wp-block-list">
<li>A bundle is a versioned artifact that declares a set of presets, extensions, workflows, and steps to install together. Bundles don’t create new primitives — they package and pin existing ones so teams get a tested configuration with one command.</li>



<li>Bundles record provenance of what they installed for clean removal and upgrades.</li>
</ul>



<p class="wp-block-paragraph">Example bundle manifest<br />File: .specify/bundles/backend-team.yaml</p>



<pre class="wp-block-code"><code>id: backend-team
version: 1.0.0
name: "Backend Team Starter"
description: "Preset, extensions, and workflows for backend engineers."

presets:
  - company-style@10
  - backend-layout@5

extensions:
  - git@2.3.0
  - jira@1.1.0

workflows:
  - sdd-pipeline@1.0.0

metadata:
  installedBy: "bundle-maintainer@example.com"
  date: "2026-10-01"</code></pre>



<p class="wp-block-paragraph">Install / lifecycle commands</p>



<ul class="wp-block-list">
<li>Install a bundle:<br />specify bundle install backend-team<br />(alias: specify bundle add backend-team)</li>



<li>List installed bundles:<br />specify bundle list</li>



<li>Upgrade a bundle:<br />specify bundle update backend-team &#8211;version 1.1.0</li>



<li>Uninstall/remove a bundle:<br />specify bundle remove backend-team</li>
</ul>



<p class="wp-block-paragraph"><strong>What bundles actually do in a repo</strong></p>



<ul class="wp-block-list">
<li>Install referenced presets, extensions, and workflows into the project.</li>



<li>Create metadata in .specify/bundles/ to record installed components and pinned versions.</li>



<li>Allow a single remove command to uninstall everything the bundle introduced.</li>
</ul>



<p class="wp-block-paragraph"><strong>When to use a bundle</strong></p>



<ul class="wp-block-list">
<li>Onboarding new hires or new projects with a single command.</li>



<li>Ensuring consistent, pinned tooling across teams.</li>



<li>Distributing proven configurations for compliance or role-specific setups.</li>
</ul>



<p class="wp-block-paragraph">Bundles: VS Code experience</p>



<ul class="wp-block-list">
<li>Terminal:
<ol class="wp-block-list">
<li>Open integrated terminal (Ctrl+`).</li>



<li>Run: specify bundle install backend-team</li>
</ol>
</li>



<li>UI (Spec Kit Companion):
<ol class="wp-block-list">
<li>Bundles view → Search → backend-team → Install.</li>



<li>The UI shows progress and created files; it lists the installed components in a panel.</li>
</ol>
</li>
</ul>



<p class="wp-block-paragraph">Security, testing, and operational guidance<br />Vetting extensions</p>



<ul class="wp-block-list">
<li>Treat extensions like any plugin: require code review, run static analysis, and audit network permissions.</li>



<li>Use least-privilege: only grant network access to specific hosts used by the extension.</li>



<li>Use CI sandboxing: run untrusted extensions in CI jobs with restricted tokens and ephemeral credentials.</li>
</ul>



<p class="wp-block-paragraph"><strong>Testing workflows and presets</strong></p>



<ul class="wp-block-list">
<li>Presets: test by generating artifacts locally and comparing before/after outputs. Use a diff tool to confirm only intended changes.</li>



<li>Workflows: dry-run where possible. Validate inputs and run with small, safe inputs. Test gate behavior by simulating approvals.</li>



<li>Extensions: unit tests and integration tests against mocked APIs. Provide a local test harness script.</li>
</ul>



<p class="wp-block-paragraph"><strong>Bundles and provenance</strong></p>



<ul class="wp-block-list">
<li>Always pin versions in bundle manifests to avoid unexpected breaking changes.</li>



<li>Use bundle metadata to record who installed the bundle and when to enable audits and rollbacks.</li>
</ul>



<p class="wp-block-paragraph">Programmatic integration: invoking specify from C#<br />If you need to call the specify CLI from other tools or CI, capture stdout/stderr. Example (async):</p>



<pre class="wp-block-code"><code>using System.Diagnostics;

var psi = new ProcessStartInfo {
    FileName = "specify",
    Arguments = "workflow run sdd-pipeline --input spec=\"Build login\" --json",
    RedirectStandardOutput = true,
    RedirectStandardError = true,
    UseShellExecute = false,
    CreateNoWindow = true
};

using var proc = Process.Start(psi);
string stdout = await proc.StandardOutput.ReadToEndAsync();
string stderr = await proc.StandardError.ReadToEndAsync();
proc.WaitForExit();
// stdout contains run result (or --json output); stderr contains errors.</code></pre>



<p class="wp-block-paragraph">In VS Code, run this same program from the integrated terminal or incorporate it into a task that runs on save or during CI.</p>



<p class="wp-block-paragraph">VS Code practical steps — exact actions users will see</p>



<ul class="wp-block-list">
<li>Open Command Palette (Ctrl+Shift+P). Search “Spec Kit: Install Preset” or “Spec Kit: Run Workflow”.</li>



<li>Spec Kit Companion sidebar panels:
<ul class="wp-block-list">
<li>Presets: search, preview, install.</li>



<li>Extensions: list, view commands, install.</li>



<li>Workflows: catalog view, run, see run state and step logs.</li>



<li>Bundles: browse, install, view installed metadata.</li>
</ul>
</li>



<li>Terminals use standard CLI commands; the UI calls the same CLI under the hood.</li>
</ul>



<p class="wp-block-paragraph">Authoring tutorials (concise walkthroughs)<br />Preset authoring walkthrough (quick)</p>



<ol class="wp-block-list">
<li>Create file: .specify/presets/company-style.yaml with your overrides.</li>



<li>Test: specify plan &#8211;input spec=&#8221;Add login&#8221; and confirm the summary and security sections are replaced.</li>



<li>Publish to catalog and instruct teams: specify preset add <a href="mailto:company-style@1.0.0">company-style@1.0.0</a>.</li>
</ol>



<p class="wp-block-paragraph">Extension authoring walkthrough (quick)</p>



<ol class="wp-block-list">
<li>Scaffold a small Node or shell extension and manifest (extension.yaml).</li>



<li>Add a command that runs a script to call Jira’s API (use environment tokens).</li>



<li>Test by installing locally: specify extension add ./my-extension.</li>



<li>Publish after security review.</li>
</ol>



<p class="wp-block-paragraph">Workflow authoring walkthrough (quick)</p>



<ol class="wp-block-list">
<li>Start from sdd-pipeline.yml example above.</li>



<li>Run locally: specify workflow run ./sdd-pipeline.yml &#8211;input spec=&#8221;&#8230;&#8221;</li>



<li>Step through the run in VS Code and fix templating issues.</li>



<li>Publish and add to a bundle for distribution.</li>
</ol>



<p class="wp-block-paragraph">Bundle authoring walkthrough (quick)</p>



<ol class="wp-block-list">
<li>Create bundle manifest listing pinned components.</li>



<li>Run: specify bundle install ./backend-team.yaml locally to validate the install process.</li>



<li>Publish the bundle to the catalog. Team members then run specify bundle install backend-team.</li>
</ol>



<p class="wp-block-paragraph"><strong>Migration patterns and practical advice</strong></p>



<ul class="wp-block-list">
<li>Start with presets when you only need wording changes (low risk).</li>



<li>When you find repeating scripts or ad-hoc tools across repos, move them into an extension.</li>



<li>When a sequence of steps becomes common and should be reproducible, author a workflow.</li>



<li>When multiple teams need the same combination, wrap all pieces into a bundle.</li>
</ul>



<p class="wp-block-paragraph"><strong>FAQ / Troubleshooting quick hits</strong></p>



<ul class="wp-block-list">
<li>Preset not applied? Check priority: specify preset list shows installed presets and priority.</li>



<li>Workflow step failed — how to debug? View step logs in the CLI run output or VS Code run panel; reproduce the shell step locally.</li>



<li>How do I remove a bundle? specify bundle remove will uninstall components that bundle recorded.</li>



<li>An extension requires network access — is it safe? Review its permissions in the manifest and audit its code. Use least-privilege credentials.</li>
</ul>



<p class="wp-block-paragraph"><strong>Glossary</strong></p>



<ul class="wp-block-list">
<li>Preset: content and prompt overrides for templates and text.</li>



<li>Extension: plugin that adds commands, hooks, and runtime behavior.</li>



<li>Workflow: a persisted YAML pipeline combining steps, shell commands, and gates.</li>



<li>Bundle: a versioned package that installs presets, extensions, and workflows together.</li>
</ul>



<p class="wp-block-paragraph"><strong>Quick CLI reference</strong></p>



<ul class="wp-block-list">
<li>Add preset: specify preset add</li>



<li>Add extension: specify extension add</li>



<li>Add workflow from catalog: specify workflow add</li>



<li>Run workflow: specify workflow run &#8211;input spec=&#8221;&#8230;&#8221;</li>



<li>Run local workflow YAML: specify workflow run ./my-workflow.yml &#8211;input spec=&#8221;&#8230;&#8221;</li>



<li>Install bundle: specify bundle install</li>



<li>List bundles: specify bundle list</li>
</ul>



<p class="wp-block-paragraph">Closing and next steps<br />Presets, extensions, workflows, and bundles complement each other:</p>



<ul class="wp-block-list">
<li>Presets shape content and tone.</li>



<li>Extensions implement behavior and integrations.</li>



<li>Workflows orchestrate repeatable, resumable processes.</li>



<li>Bundles distribute curated stacks and keep provenance.</li>
</ul>



<p class="wp-block-paragraph">Note: The initial draft of this post was written by <a href="https://github.com/JesseLiberty/BlogWriter/blob/main/README.md">BlogWriter</a> and then<br />edited by Jesse Liberty.<br />Illustrations by Copilot. Caution: LLMs make mistakes; this post is offered as is.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Prompts in MCP Servers</title>
		<link>https://jesseliberty.com/2026/10/07/prompts-in-mcp-servers/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Wed, 07 Oct 2026 17:19:04 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[Essentials]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13839</guid>

					<description><![CDATA[TL;DR Prompts allow you to manage the exact instructions your chat models receive and let apps pick those instructions at runtime. That&#8217;s the core value of “prompts” in the Model Context Protocol (MCP). MCP prompts let a server publish reusable, &#8230; <a href="https://jesseliberty.com/2026/10/07/prompts-in-mcp-servers/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">TL;DR</p>



<p class="wp-block-paragraph">Prompts allow you to manage the exact instructions your chat models receive and let apps pick those instructions at runtime.</p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="585" height="800" src="https://jesseliberty.com/wp-content/uploads/2026/10/prompts-585x800.jpg" alt="" class="wp-image-13843" style="aspect-ratio:0.7312447508740405;width:327px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/10/prompts-585x800.jpg 585w, https://jesseliberty.com/wp-content/uploads/2026/10/prompts-110x150.jpg 110w, https://jesseliberty.com/wp-content/uploads/2026/10/prompts-219x300.jpg 219w, https://jesseliberty.com/wp-content/uploads/2026/10/prompts-767x1049.jpg 767w, https://jesseliberty.com/wp-content/uploads/2026/10/prompts.jpg 932w" sizes="auto, (max-width: 585px) 100vw, 585px" /></figure>



<p class="wp-block-paragraph">That&#8217;s the core value of “prompts” in the Model Context Protocol (MCP). MCP prompts let a server publish reusable, argument‑driven prompt templates that clients (including Microsoft Agent Framework agents) can discover, retrieve, and use in LLM chat calls—so behavior, UX, and auditing are consistent across apps.</p>



<span id="more-13839"></span>



<p class="wp-block-paragraph">Key terms (quick definitions)</p>



<ul class="wp-block-list">
<li>MCP server: a service that implements the Model Context Protocol and exposes tools, prompts, and resources to clients.</li>



<li>Prompt template (MCP “prompt”): a server‑hosted, reusable instruction template that can accept named arguments and return one or more chat messages.</li>



<li>Prompt arguments: named values (e.g., url, language, timeframe) supplied by the client when calling prompts/get.</li>



<li>Prompt response messages: the chat messages (roles like system/user/assistant) returned by prompts/get that are ready to be appended to a model call.</li>



<li>Resource messages: special messages in a prompt response that contain URIs or attachments (files, repo links) the model should consider.</li>
</ul>



<p class="wp-block-paragraph">Why MCP prompts matter for developers and product teams</p>



<ul class="wp-block-list">
<li>Central control: product teams can update a prompt template on the server and clients will automatically get the new behavior the next time they fetch it.</li>



<li>Consistency: multiple agents and apps use the same instructions and checklists (e.g., compliance templates, code‑review checklists).</li>



<li>Reusability: prompts are parameterized. One server prompt can cover many scenarios by taking arguments.</li>



<li>Discoverability &amp; UX: prompts/list lets clients surface available commands (slash menus, command palettes) rather than hardcoding instructions into each client.</li>



<li>Auditability: when agents log which prompt and which arguments were used, teams can trace model behavior back to a server‑controlled template.</li>
</ul>



<p class="wp-block-paragraph">How prompts fit into the Microsoft Agent Framework<br />The Microsoft Agent Framework can connect to MCP servers at runtime. That allows an agent to:</p>



<ol class="wp-block-list">
<li>Call prompts/list to discover server prompts and show them to the user (for example, as a slash‑command menu).</li>



<li>Prompt the user for required arguments (URL, code snippet, language).</li>



<li>Call prompts/get (name + arguments) to fetch the populated template as chat messages.</li>



<li>Append the returned messages to the chat history and send them to the LLM client (e.g., Azure OpenAI).<br />Because the prompt is server‑hosted, the server controls wording, embedded resources, and any instruction to call tools—clients only provide arguments and deliver the resulting messages to the model.</li>
</ol>



<p class="wp-block-paragraph">Typical flow (concise)</p>



<ol class="wp-block-list">
<li>Discover: prompts/list → display options.</li>



<li>Select &amp; collect args: user picks a prompt → agent collects required fields.</li>



<li>Retrieve: prompts/get(name, args) → server returns promptContent.Messages and optionally resource messages.</li>



<li>Use: append returned messages to your chat request and send to the model.</li>
</ol>



<p class="wp-block-paragraph">Sample prompts/get JSON (small example)<br />This is a minimal illustration of what prompts/get might return.<br />{<br />&#8220;messages&#8221;: [<br />{ &#8220;role&#8221;: &#8220;system&#8221;, &#8220;content&#8221;: &#8220;You are an expert summarizer. Focus on key findings and risks.&#8221; },<br />{ &#8220;role&#8221;: &#8220;user&#8221;, &#8220;content&#8221;: &#8220;Summarize the document at: <a href="https://example.com/report.pdf">https://example.com/report.pdf</a>&#8221; }<br />],<br />&#8220;resources&#8221;: [<br />{ &#8220;type&#8221;: &#8220;uri&#8221;, &#8220;uri&#8221;: &#8220;<a href="https://example.com/report.pdf">https://example.com/report.pdf</a>&#8220;, &#8220;description&#8221;: &#8220;Source PDF&#8221; }<br />]<br />}</p>



<p class="wp-block-paragraph">C# examples (illustrative, practical, with error handling)<br />Notes: the exact MCP SDK namespace and method names vary by SDK version. The examples below follow common patterns used in the MCP quickstart and show how to map MCP messages into a chat client call. Replace transport/creation code with the actual constructors in your MCP SDK.</p>



<p class="wp-block-paragraph">Install (example)</p>



<ul class="wp-block-list">
<li>Add an MCP client package (example package id: ModelContextProtocol.Client)</li>



<li>Add your LLM client package (example: Azure.AI.OpenAI)</li>
</ul>



<p class="wp-block-paragraph">Example 1 — Summarize document (argument: url)<br />This example lists prompts, finds summarize-document, calls get with a URL, and sends returned messages to an Azure OpenAI chat client.</p>



<p class="wp-block-paragraph"></p>



<pre class="wp-block-preformatted">using System;<br />using System.Collections.Generic;<br />using System.Linq;<br />using System.Threading;<br />using System.Threading.Tasks;<br />using ModelContextProtocol.Client; // example package<br />using Azure.AI.OpenAI;<br />using Azure;<br /><br />async Task SummarizeDocumentAsync(string documentUrl, CancellationToken cancellationToken = default)<br />{<br />try<br />{<br />// 1) Create MCP client (transport creation is SDK-specific)<br />var transport = /* create transport per your SDK: HTTP, WebSocket, etc. */;<br />var mcpClient = await McpClient.CreateAsync(transport, cancellationToken);<code> // 2) List prompts<br />    var prompts = await mcpClient.ListPromptsAsync(cancellationToken);<br />    var summarize = prompts.FirstOrDefault(p => p.Name == "summarize-document");<br />    if (summarize == null)<br />        throw new InvalidOperationException("Prompt 'summarize-document' not available.");<br /><br />    // 3) Call prompts/get with required arguments<br />    var args = new Dictionary&lt;string, object> { ["url"] = documentUrl };<br />    var promptContent = await summarize.GetAsync(args, cancellationToken);<br /><br />    // 4) Map MCP messages to OpenAI chat messages<br />    var openAiClient = new OpenAIClient(new Uri("https://your-azure-endpoint/"), new AzureKeyCredential("AZURE_KEY"));<br />    var chatOptions = new ChatCompletionsOptions();<br />    foreach (var m in promptContent.Messages)<br />    {<br />        chatOptions.Messages.Add(MapMcpMessageToChatMessage(m));<br />    }<br /><br />    // Optional: append follow-up instructions from the user<br />    chatOptions.Messages.Add(new ChatMessage(ChatRole.User, "Please keep the summary under 200 words."));<br /><br />    var response = await openAiClient.GetChatCompletionsAsync("deployment-name", chatOptions, cancellationToken);<br />    Console.WriteLine(response.Value.Choices[0].Message.Content);<br />}<br />catch (RequestFailedException ex) when (ex.Status == 404)<br />{<br />    Console.Error.WriteLine("Prompt not found: " + ex.Message);<br />}<br />catch (Exception ex)<br />{<br />    Console.Error.WriteLine("Error fetching prompt or calling model: " + ex.Message);<br />    throw;<br />}</code>}</pre>



<pre class="wp-block-code"><code>static ChatMessage MapMcpMessageToChatMessage(McpMessage m)
{
var role = m.Role switch
{
  system" => ChatRole.System,
  "assistant" => ChatRole.Assistant,
  _ => ChatRole.User
};
  return new ChatMessage(role, m.Content);
}</code></pre>



<p class="wp-block-paragraph">Example 2 — Code review (arguments: code, optional language)<br />This example demonstrates sending a code snippet and language argument to a code_review prompt. The server might return a checklist system message plus resource URIs pointing to related files.</p>



<pre class="wp-block-code"><code>async Task CodeReviewAsync(string codeSnippet, string language = "csharp", CancellationToken cancellationToken = default)
{
var transport = /* create transport */;
var mcpClient = await McpClient.CreateAsync(transport, cancellationToken);
var prompts = await mcpClient.ListPromptsAsync(cancellationToken);
var codePrompt = prompts.FirstOrDefault(p => p.Name == "code_review");
if (codePrompt == null) throw new InvalidOperationException("code_review not available");

var args = new Dictionary&lt;string, object> { &#91;"code"] = codeSnippet, &#91;"language"] = language };
var promptContent = await codePrompt.GetAsync(args, cancellationToken);

var openAiClient = new OpenAIClient(new Uri("https://your-azure-endpoint/"), new AzureKeyCredential("KEY"));
var chatOptions = new ChatCompletionsOptions();
foreach (var m in promptContent.Messages)
{
    chatOptions.Messages.Add(MapMcpMessageToChatMessage(m));
}

var response = await openAiClient.GetChatCompletionsAsync("deployment-name", chatOptions, cancellationToken);
Console.WriteLine(response.Value.Choices&#91;0].Message.Content);
}</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">Practical tips (concrete, actionable)</p>



<ul class="wp-block-list">
<li>Validate arguments on the client: whitelist allowed languages, enforce URL schemes (https), and check length limits. Refuse or sanitize inputs that look like secrets.</li>



<li>Prefer proxying or previewing third‑party resources instead of sending raw URIs to external servers. If you must share URIs, use signed, time‑limited links.</li>



<li>Handle common error codes: 404 = prompt not found, 400 = missing/invalid args, 429 = rate limit; implement exponential backoff and circuit breakers.</li>



<li>Cache prompt metadata: calls to prompts/list are relatively cheap but cache names/descriptions and refresh periodically to avoid UI delays.</li>



<li>UI recommendations: present prompts in a command palette or slash‑menu; show required arguments and a small template preview before fetching; allow an editable preview so users can tweak arguments before sending.</li>



<li>Embedded resources: if the prompt returns resource messages, decide whether to fetch the resource text (and include it in the chat) or attach a link. If fetching, enforce size limits and sanitize content.</li>



<li>Versioning &amp; breaking changes: because prompts are server‑controlled, add an ability to pin a prompt version (if the server supports it) or keep integration tests that validate prompt behavior after server updates.</li>



<li>Audit and tracing: log prompt name + non‑sensitive args and add trace IDs/headers when calling MCP servers so you can correlate user actions and prompt outputs for debugging and compliance.</li>
</ul>



<p class="wp-block-paragraph">Security &amp; operational specifics</p>



<ul class="wp-block-list">
<li>Redact secrets: never send API keys, passwords, or PII to a third‑party MCP server. Run argument sanitization and redaction on the client.</li>



<li>Signed URLs: use expiring signed URLs when providing attachments to external servers; this limits exposure.</li>



<li>Policy &amp; approval: treat server‑hosted prompts as part of your product’s compliance surface. Establish a governance process for who can change server prompts.</li>



<li>Headers &amp; audit: follow the Agent Framework guidance for adding audit headers and tracing to MCP calls. Store logs of prompt usage (prompt name, timestamp, actor) with care for privacy.</li>
</ul>



<p class="wp-block-paragraph">Error handling &amp; retries (short patterns)</p>



<ul class="wp-block-list">
<li>Use CancellationToken for timeouts.</li>



<li>Retry transient errors (HTTP 5xx, 429) with an exponential backoff and jitter.</li>



<li>Surface user‑friendly messages for 400/422 responses (missing args or invalid input) and guide the user to correct inputs.</li>
</ul>



<p class="wp-block-paragraph">Where to read next</p>



<ul class="wp-block-list">
<li>Model Context Protocol prompts spec: modelcontextprotocol.io (prompts/list and prompts/get shapes).</li>



<li>Microsoft Agent Framework docs: on integrating MCP prompts and security guidance.</li>



<li>.NET quickstart and MCP C# SDK samples: for precise types and SDK usage in production.</li>
</ul>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>When Is the LLM Called?</title>
		<link>https://jesseliberty.com/2026/10/06/when-is-the-llm-called/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Tue, 06 Oct 2026 19:07:56 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13835</guid>

					<description><![CDATA[When working with agents (e.g., in Microsoft Agent Framework) it is easy to be confused as to when the LLM is called. Since calling the LLM (and getting responses) costs money (in the form of expended tokens) it is important &#8230; <a href="https://jesseliberty.com/2026/10/06/when-is-the-llm-called/">Continue reading <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">When working with agents (e.g., in Microsoft Agent Framework) it is easy to be confused as to when the LLM is called. Since calling the LLM (and getting responses) costs money (in the form of expended tokens) it is important to understand this relationship.</p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="800" height="799" src="https://jesseliberty.com/wp-content/uploads/2026/10/slots-800x799.jpg" alt="" class="wp-image-13836" style="aspect-ratio:1.001237565964601;width:410px;height:auto" srcset="https://jesseliberty.com/wp-content/uploads/2026/10/slots-800x799.jpg 800w, https://jesseliberty.com/wp-content/uploads/2026/10/slots-150x150.jpg 150w, https://jesseliberty.com/wp-content/uploads/2026/10/slots-300x300.jpg 300w, https://jesseliberty.com/wp-content/uploads/2026/10/slots-768x767.jpg 768w, https://jesseliberty.com/wp-content/uploads/2026/10/slots.jpg 965w" sizes="auto, (max-width: 800px) 100vw, 800px" /></figure>



<p class="wp-block-paragraph">An agent in the Microsoft Agent Framework (MAF) talks to an LLM whenever it needs to: <br />(1) decide what to do or generate text (initial planning/response generation), <br />(2) decide whether to call a tool and provide its arguments (function/tool calling), and <br />(3) incorporate tool outputs and continue reasoning or synthesize the final reply. </p>



<p class="wp-block-paragraph">A single agent run commonly produces multiple model calls in a loop: planning → tool call(s) → synthesis → (repeat if needed).</p>



<span id="more-13835"></span>



<p class="wp-block-paragraph">MAF implements a layered agent pipeline. Calling agent.RunAsync (or equivalent) starts a run that flows down middleware and context providers before ever hitting a model client (the LLM), and the run’s responses flow back up. That orchestration is why an agent doesn’t “just call the LLM once” by default—the framework determines when and how many model requests are required based on the agent’s instructions, tools, and middleware.</p>



<p class="wp-block-paragraph"><strong>Three middleware layers you can hook in</strong></p>



<ul class="wp-block-list">
<li>Agent-run middleware: wraps the entire run (fires once per run). Good for run-level telemetry or cancellation.</li>



<li>Function/tool middleware: wraps each tool invocation (fires for each tool call). Useful for gating tool execution, approvals, or stubbing tools for tests.</li>



<li>Chat / IChatClient middleware: wraps every underlying model/chat client call (fires every time the framework calls the model). Useful for logging prompts, redaction, or cost measurement.</li>
</ul>



<p class="wp-block-paragraph"><strong>Typical sequence that triggers LLM calls</strong></p>



<ol class="wp-block-list">
<li>Request received → pipeline and context providers populate run context (no model call yet).</li>



<li>Initial reasoning/plan → LLM call to decide plan (generate response or choose a tool). This is typically the first chat call.</li>



<li>If the model chooses a tool → framework executes function middleware and runs the tool (tool execution itself may be non-LLM code).</li>



<li>After tool returns → agent calls LLM again to integrate tool output and produce/synthesize the final response.</li>



<li>If needed, the LLM may ask to call another tool and the loop continues. Each cycle can add one (or more, depending on provider semantics) model calls.</li>
</ol>



<p class="wp-block-paragraph"><strong>C# examples</strong></p>



<ol class="wp-block-list">
<li>Minimal agent run (RunAsync typically triggers at least one LLM call)<br />This follows the quick-start pattern; RunAsync goes through the pipeline and will call the model as needed.</li>
</ol>



<pre class="wp-block-code"><code>using System;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;

var agent = new AIProjectClient(
new Uri("https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"),
new AzureCliCredential()
).AsAIAgent(
model: "gpt-5.4-mini",
instructions: "You are a helpful assistant. Keep answers brief."
);

var result = await agent.RunAsync("What is the largest city in France?");
Console.WriteLine(result);</code></pre>



<ol start="2" class="wp-block-list">
<li>Tool/function call flow (model decides to call a tool → tool executes → model synthesizes)<br />Sketch of registering a tool; the run will typically perform model → tool → model:</li>
</ol>



<pre class="wp-block-code"><code>// sketch
var myTool = new FunctionTool(
name: "get-weather",
description: "Get weather for a city",
handler: async (args) => {
return await GetWeatherAsync(args); // actual non-LLM work
}
);

var agent = new ChatClientAgentBuilder()
.UseModel("gpt-5.x")
.RegisterTool(myTool)
.Build();

// This RunAsync will likely:
// 1) call model to decide plan (may return "call get-weather")
// 2) execute myTool (no LLM)
// 3) call model to synthesize the final reply
var response = await agent.RunAsync("What's the weather in Seattle tomorrow?");
Console.WriteLine(response);</code></pre>



<ol start="3" class="wp-block-list">
<li>Chat middleware: intercept every LLM call (log each model invocation)<br />Chat-level middleware runs on every model/chat client request:</li>
</ol>



<pre class="wp-block-code"><code>async Task LoggingChatMiddleware(
IEnumerable messages,
ChatOptions? options,
IChatClient innerClient,
CancellationToken cancellationToken)
{
  Console.WriteLine($"&#91;ChatMiddleware] Sending {messages.Count()} messages to model...");
  var resp = await innerClient.GetResponseAsync(messages, options, cancellationToken);
  Console.WriteLine($"&#91;ChatMiddleware] Model returned {resp.Messages.Count} messages.");
  return resp;
}

// Register this middleware when configuring IChatClient / ChatClientAgent (see docs for exact APIs).</code></pre>



<ol start="4" class="wp-block-list">
<li>Agent-run and function middleware (brief sketches)</li>
</ol>



<ul class="wp-block-list">
<li>Agent-run middleware: wrap whole run for telemetry or per-run constraints.</li>



<li>Function middleware: intercept each tool invocation (e.g., require approval):</li>
</ul>



<pre class="wp-block-code"><code>// Function middleware sketch
async Task ApproveThenExecuteMiddleware(
ToolInvocation invocation,
Func&lt;ToolInvocation, Task> next,
CancellationToken ct)
{
  if (!IsApproved(invocation)) throw new Exception("Tool call not approved.");
    return await next(invocation); // proceed to actual tool handler
}</code></pre>



<p class="wp-block-paragraph">Practical caveats and best practices</p>



<ul class="wp-block-list">
<li><em>Multiple LLM calls per run are normal</em>. Instrument chat middleware to measure API usage and cost.</li>



<li>Tool execution is non-LLM work; function middleware can gate, mock, or log tool runs.</li>



<li>Providers differ: function-calling semantics, streaming behavior, and limits can change how often or how responses are streamed.</li>



<li>Use concise instructions and tool schemas so the model makes fewer unnecessary tool calls.</li>



<li>If you need strict accounting of model usage, intercept at chat middleware (every model call passes there).</li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>1,500 posts</title>
		<link>https://jesseliberty.com/2026/10/06/1500-posts/</link>
		
		<dc:creator><![CDATA[Jesse Liberty]]></dc:creator>
		<pubDate>Tue, 06 Oct 2026 15:30:32 +0000</pubDate>
				<category><![CDATA[Essentials]]></category>
		<guid isPermaLink="false">https://jesseliberty.com/?p=13833</guid>

					<description><![CDATA[So far, this blog has published 1,500 posts.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">So far, this blog has published 1,500 posts.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>