<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hagicode</title>
    <description>The latest articles on DEV Community by Hagicode (@newbe36524).</description>
    <link>https://dev.to/newbe36524</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F588826%2Ff5bd5c70-a7e9-435d-b87c-43c73d4cff66.png</url>
      <title>DEV Community: Hagicode</title>
      <link>https://dev.to/newbe36524</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/newbe36524"/>
    <language>en</language>
    <item>
      <title>Steamworks Multilingual Metadata Management: From Manual Maintenance to Structured Workflow</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Sat, 09 May 2026 09:00:07 +0000</pubDate>
      <link>https://dev.to/newbe36524/steamworks-multilingual-metadata-management-from-manual-maintenance-to-structured-workflow-2i9p</link>
      <guid>https://dev.to/newbe36524/steamworks-multilingual-metadata-management-from-manual-maintenance-to-structured-workflow-2i9p</guid>
      <description>&lt;h1&gt;
  
  
  Steamworks Multilingual Metadata Management: From Manual Maintenance to Structured Workflow
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;The Steam platform requires games to provide store descriptions in 28 languages. Traditional manual maintenance is inefficient and error-prone. This article introduces how to build a structured multilingual metadata management system through HagiCode, achieving an integrated workflow from content creation to export and release.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;The Steam platform requires games and applications to provide multilingual store descriptions, including fields like &lt;code&gt;about&lt;/code&gt; (detailed description) and &lt;code&gt;short_description&lt;/code&gt; (short description). For products released globally, localization content in 28 languages is typically required.&lt;/p&gt;

&lt;p&gt;This sounds like a simple content management task, but when you actually start working on it, you discover there are more problems than you imagined.&lt;/p&gt;

&lt;p&gt;First, the maintenance workload is enormous. 28 languages multiplied by 2 fields equals 56 content blocks that need to be managed. Manually switching languages for editing in the Steamworks website backend is indeed inefficient. Every content update requires repeating this process—it's painful to even talk about it.&lt;/p&gt;

&lt;p&gt;Second, scattered content is difficult to manage. Multilingual content is typically scattered across different tools and documents, lacking a unified local storage format. Version control becomes difficult, and team collaboration is prone to errors. After all, scattered things are like scattered memories—when you want to find them, you can't.&lt;/p&gt;

&lt;p&gt;Furthermore, DLC content and main application content management are siloed. If your game has multiple DLCs, each DLC needs to maintain multilingual content separately, and management complexity grows exponentially. It's like life—things pile up, and you don't know where to start cleaning up.&lt;/p&gt;

&lt;p&gt;Finally, the export format is unintuitive. The JSON format required by Steamworks doesn't match human reading habits, making manual editing error-prone. After all, who wants to look at that dense JSON?&lt;/p&gt;

&lt;p&gt;These were all problems we encountered during the actual development of the HagiCode project. As an AI coding tool for global development, we need to maintain complete multilingual content for the Steam platform. Traditional maintenance methods could no longer meet our needs, and we urgently needed a more efficient solution. Actually, there's no other way—we had to build it ourselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution shared in this article comes from our practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode is an AI coding tool that supports multiple AI providers and code editors. During development, we needed to maintain multilingual store content for the Steam platform, which drove us to build a structured metadata management system.&lt;/p&gt;

&lt;p&gt;The multilingual metadata management solution shared in this article is exactly what we actually refined through trial and optimization during HagiCode development. If you find this solution valuable, it shows our engineering strength is pretty good—so HagiCode itself is worth paying attention to. After all, a tool that can solve problems is a good tool, right?&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Concepts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Languages and Fields
&lt;/h3&gt;

&lt;p&gt;Steamworks supports a fairly complete list of languages, covering major markets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;en-US, fr-FR, it-IT, de-DE, es-ES,
bg-BG, cs-CZ, da-DK, nl-NL, fi-FI,
el-GR, hu-HU, id-ID, ja-JP, ko-KR,
nb-NO, pl-PL, pt-BR, pt-PT, ro-RO,
ru-RU, zh-CN, es-419, sv-SE, th-TH,
zh-TW, tr-TR, uk-UA, vi-VN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most commonly used are &lt;code&gt;en-US&lt;/code&gt; (English), &lt;code&gt;zh-CN&lt;/code&gt; (Simplified Chinese), &lt;code&gt;zh-TW&lt;/code&gt; (Traditional Chinese), &lt;code&gt;ja-JP&lt;/code&gt; (Japanese), and &lt;code&gt;ko-KR&lt;/code&gt; (Korean). After all, these languages cover major markets—once you get these done, the others aren't so scary.&lt;/p&gt;

&lt;p&gt;The main fields that need to be maintained include two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;about&lt;/code&gt;: Detailed description, supports rich text format&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;short_description&lt;/code&gt;: Short description, with a 300-character limit&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Scope Concept
&lt;/h3&gt;

&lt;p&gt;Steam app content can be divided into two scopes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Base App&lt;/strong&gt;: Main application content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DLC&lt;/strong&gt;: Downloadable content, each DLC has independent content management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This distinction is important because DLCs typically need independent store descriptions, and a game may have multiple DLCs that need unified management. It's like life—some things are primary, some are additional, but they all need to be managed properly, or things become a mess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Model Design
&lt;/h2&gt;

&lt;p&gt;The system defines a clear data model to support multilingual content management:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 28 supported language codes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STEAMWORKS_SUPPORTED_LOCALES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fr-FR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;it-IT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;de-DE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es-ES&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-BG&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cs-CZ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;da-DK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nl-NL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fi-FI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;el-GR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hu-HU&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id-ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ja-JP&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ko-KR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nb-NO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pl-PL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pt-BR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pt-PT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ro-RO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ru-RU&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es-419&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sv-SE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;th-TH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh-TW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tr-TR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uk-UA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vi-VN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Supported fields&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STEAMWORKS_SUPPORTED_FIELDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;about&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// Detailed description&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;short_description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// Short description&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Content scope&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SteamworksScopeKind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dlc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few considerations in this model design—well, actually, it's just about making things a bit simpler:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use standard language code formats (like &lt;code&gt;zh-CN&lt;/code&gt; instead of &lt;code&gt;chinese&lt;/code&gt;)—after all, standard things are always more reliable&lt;/li&gt;
&lt;li&gt;Explicitly list field types for future extension—who knows if more fields will be needed later&lt;/li&gt;
&lt;li&gt;Distinguish scope types to support unified management of Base App and DLC—it's always good to keep things clear&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  File Storage Structure
&lt;/h2&gt;

&lt;p&gt;Content is stored in &lt;code&gt;.hagiclaw-data/steamworks-metadata/&lt;/code&gt; in the project directory, using a hierarchical directory structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.hagiclaw-data/
└── steamworks-metadata/
    └── default-app/
        ├── workspace.json              # Workspace configuration manifest
        ├── base/                       # Base application content
        │   ├── en-US/
        │   │   ├── about.md
        │   │   └── short_description.md
        │   ├── zh-CN/
        │   │   ├── about.md
        │   │   └── short_description.md
        │   └── ...
        └── dlc/                        # DLC content
            └── turbo-engine/
                ├── en-US/
                │   ├── about.md
                │   └── short_description.md
                └── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure design has several advantages—or at least, it's much better than the previous approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Human-readable&lt;/strong&gt;: Each content is an independent Markdown file that can be edited directly—after all, human eyes prefer to see things clearly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version control friendly&lt;/strong&gt;: Text files make it easy to track change history and compare differences—so what was changed is clear at a glance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strong extensibility&lt;/strong&gt;: Adding new languages or fields only requires creating new files—like building blocks, add whatever you want&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear structure&lt;/strong&gt;: The directory structure intuitively reflects how content is organized—won't make people feel confused&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;workspace.json&lt;/code&gt; stores workspace configuration, including DLC list and language configuration information. After all, some things still need a manifest—otherwise, after a while, who remembers what they put where.&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdown to BBCode Conversion
&lt;/h2&gt;

&lt;p&gt;Steam uses BBCode format for rich text, not standard Markdown. This brings additional workload to content creation—either write BBCode directly or manually convert it later.&lt;/p&gt;

&lt;p&gt;HagiCode's solution is: let developers create content in familiar Markdown, and the system automatically converts it to Steam BBCode. After all, people are always accustomed to what they're familiar with—why force yourself to adapt to those strange curly braces?&lt;/p&gt;

&lt;h3&gt;
  
  
  Conversion Rules
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Heading conversion&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;HagiCode&lt;/span&gt;        &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;HagiCode&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;]
##&lt;/span&gt; &lt;span class="nx"&gt;Features&lt;/span&gt;        &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;Features&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/h2&lt;/span&gt;&lt;span class="err"&gt;]
&lt;/span&gt;
&lt;span class="c1"&gt;// Text styles&lt;/span&gt;
&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="nx"&gt;bold&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;     &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;bold&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/b&lt;/span&gt;&lt;span class="err"&gt;]
&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;italic&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;     &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;italic&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="err"&gt;]
&lt;/span&gt;&lt;span class="s2"&gt;`code`&lt;/span&gt;            &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/code&lt;/span&gt;&lt;span class="err"&gt;]
&lt;/span&gt;
&lt;span class="c1"&gt;// Links and images&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/url&lt;/span&gt;&lt;span class="err"&gt;]
&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{STEAM_APP_IMAGE}/extras/...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sr"&gt;/img&lt;/span&gt;&lt;span class="err"&gt;]
&lt;/span&gt;
&lt;span class="c1"&gt;// Lists&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;          &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
                   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wrapped&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Language Wrapping
&lt;/h3&gt;

&lt;p&gt;When exporting, content needs to be wrapped with language tags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;wrapWithSteamLanguage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SteamworksLocaleCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bbcode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Returns [lang=english]...[/lang] format&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Language codes need to be mapped to Steam's format:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;en-US&lt;/code&gt; → &lt;code&gt;english&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;zh-CN&lt;/code&gt; → &lt;code&gt;schinese&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;zh-TW&lt;/code&gt; → &lt;code&gt;tchinese&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ja-JP&lt;/code&gt; → &lt;code&gt;japanese&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ko-KR&lt;/code&gt; → &lt;code&gt;korean&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mapping relationship isn't actually that complicated, it just needs to be remembered. After all, every platform has its own rules, we can only adapt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Export Format
&lt;/h2&gt;

&lt;p&gt;The exported JSON needs to meet Steamworks' structure requirements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"itemid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1158573"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"languages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"english"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"app[content][about]"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[h1]HagiCode[/h1]&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;[b]About[/b]..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"app[content][short_description]"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AI coding tool..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schinese"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"app[content][about]"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[h1]HagiCode[/h1]&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;[b]关于[/b]..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"app[content][short_description]"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AI 编码工具..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key points aren't many, just need to remember these format requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;itemid&lt;/code&gt; corresponds to Steam AppID&lt;/li&gt;
&lt;li&gt;Steam's language codes (like &lt;code&gt;schinese&lt;/code&gt;) are used under &lt;code&gt;languages&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Field paths use &lt;code&gt;app[content][fieldName]&lt;/code&gt; format&lt;/li&gt;
&lt;li&gt;Values are converted BBCode strings&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These rules seem a bit tedious, but you get used to them. After all, every platform has its own temperament, we can only adapt.&lt;/p&gt;

&lt;h2&gt;
  
  
  API Service Design
&lt;/h2&gt;

&lt;p&gt;The system provides a complete REST API to support the multilingual content management workflow:&lt;/p&gt;

&lt;h3&gt;
  
  
  Load Workspace
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;steamworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns workspace configuration, all languages, and field content. After all, there needs to be a place to pull everything out for viewing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Save Content
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;steamworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scopeId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base-app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scopeKind&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;values&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;about&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Markdown content...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;short_description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Short text...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;about&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Markdown 内容...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;short_description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;简短文本...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When saving, the system writes Markdown content to corresponding &lt;code&gt;.md&lt;/code&gt; files. This way nothing gets lost—after all, memory is always unreliable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Render Preview
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;steamworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;preview&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;locale&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;field&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;about&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;# HagiCode&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;这是关于...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns Markdown rendering result and BBCode conversion result for easy previewing. Preview is like looking in a mirror—you should at least see what you look like before going out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Export JSON
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;steamworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scopeId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base-app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scopeKind&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generates Steamworks-format JSON that can be directly imported into the Steamworks backend. This step is essentially packaging everything up, ready for shipping.&lt;/p&gt;

&lt;h3&gt;
  
  
  DLC Management
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;steamworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;dlc&lt;/span&gt;    &lt;span class="c1"&gt;// Create&lt;/span&gt;
&lt;span class="nx"&gt;PUT&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;steamworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;dlc&lt;/span&gt;     &lt;span class="c1"&gt;// Update&lt;/span&gt;
&lt;span class="nx"&gt;DELETE&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;steamworks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;dlc&lt;/span&gt;  &lt;span class="c1"&gt;// Delete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DLC management includes creating, updating, and deleting DLC metadata configurations. After all, DLC is also content and needs to be managed properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage Workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Access Metadata Panel
&lt;/h3&gt;

&lt;p&gt;Open the Steamworks Metadata panel in the HagicLaw workspace, and the system will load the current workspace's configuration and content. Once all preparations are done, you can begin.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Select Edit Scope
&lt;/h3&gt;

&lt;p&gt;Select Base App or a specific DLC in the left navigation. Each scope independently manages its multilingual content. Like organizing a room—first categorize things, then clean them up one by one.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Multilingual Matrix Editing
&lt;/h3&gt;

&lt;p&gt;Expand the languages you need to edit, and directly edit the Markdown content for &lt;code&gt;about&lt;/code&gt; and &lt;code&gt;short_description&lt;/code&gt;. The system supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time Markdown rendering preview&lt;/li&gt;
&lt;li&gt;Steam BBCode conversion preview&lt;/li&gt;
&lt;li&gt;Character count and length checking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These preview features are actually quite useful—at least you can know what your content looks like. After all, no one wants to write a bunch of stuff only to find the format is completely wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Save Content
&lt;/h3&gt;

&lt;p&gt;Click the save button, and content is automatically written to corresponding &lt;code&gt;.md&lt;/code&gt; files. Files are included in Git version control for easy change tracking. Saving is like writing down memories—they won't be forgotten even after a long time.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Validation Checks
&lt;/h3&gt;

&lt;p&gt;The system automatically checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether required fields are complete&lt;/li&gt;
&lt;li&gt;Whether &lt;code&gt;short_description&lt;/code&gt; exceeds 300 characters&lt;/li&gt;
&lt;li&gt;Whether Markdown syntax is correct&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks can avoid some basic errors—after all, humans make mistakes, it's always good to have a machine help watch over things.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Export JSON
&lt;/h3&gt;

&lt;p&gt;Select the scope to export (Base App or specific DLC), and the system generates Steamworks JSON containing all languages. Copy the JSON and paste it into the Steamworks backend to complete the import. Once this step is done, the entire workflow is complete. Everything is ready, just waiting for release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Language Code Mapping
&lt;/h3&gt;

&lt;p&gt;The system's &lt;code&gt;en-US&lt;/code&gt; corresponds to Steam's &lt;code&gt;english&lt;/code&gt;, and &lt;code&gt;zh-CN&lt;/code&gt; corresponds to &lt;code&gt;schinese&lt;/code&gt;. This mapping is handled automatically during export, but needs attention when manually editing JSON. After all, some things machines can help you with, but some you still need to remember yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  BBCode Limitations
&lt;/h3&gt;

&lt;p&gt;Steam only supports a subset of BBCode, and complex Markdown may not convert perfectly. It's recommended to check conversion results in preview. Preview is like looking in a mirror—check what you look like before going out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Image Paths
&lt;/h3&gt;

&lt;p&gt;Images are converted to &lt;code&gt;[img src="{STEAM_APP_IMAGE}/extras/..."]&lt;/code&gt; placeholder format. Actual images need to be uploaded separately to the Steam backend. Images are sometimes more persuasive than text, just a bit more troublesome to upload.&lt;/p&gt;

&lt;h3&gt;
  
  
  Field Validation
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;short_description&lt;/code&gt; has a strict 300-character limit. The system validates before export, but it's recommended to control length during editing. After all, writing too many characters is useless—the platform only looks at the first 300, so you have to be concise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version Control
&lt;/h3&gt;

&lt;p&gt;All Markdown files can be included in Git version control for easy change history tracking and collaborative editing. It's recommended to commit changes regularly. Version control is like a time machine that lets you return to a past moment and see what you wrote then.&lt;/p&gt;

&lt;h3&gt;
  
  
  DLC Management
&lt;/h3&gt;

&lt;p&gt;DLC's &lt;code&gt;itemId&lt;/code&gt; needs to correspond to the DLC AppID in the Steamworks backend. When creating a DLC, ensure the ID is accurate. IDs are hard to change once wrong, so it's better to be careful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The core challenge of Steamworks multilingual metadata management lies in how to efficiently maintain large amounts of multilingual content. Through structured data models, human-friendly file storage, and automated conversion/export workflows, we can transform this tedious process into a manageable content creation workflow.&lt;/p&gt;

&lt;p&gt;This solution has proven effective in the practice of the HagiCode project. We transformed from a manual, error-prone state to a structured, verifiable, collaborative workflow. This not only improved efficiency but also reduced human error. After all, when the tool is well-made, things become simple.&lt;/p&gt;

&lt;p&gt;If you're developing applications for the Steam platform and need to maintain multilingual content, I hope this solution can provide some inspiration. Multilingual content management doesn't have to be a painful thing—with the right tools and workflows, it can become relatively easy. Or at least, not so despair-inducing...&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://partner.steamgames.com/doc/store/localization" rel="noopener noreferrer"&gt;Steamworks Documentation - Store Metadata&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://partner.steamgames.com/doc/store/additional_description/steamlocalization#bbcode" rel="noopener noreferrer"&gt;Steam BBCode Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;HagiCode project: &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;github.com/HagiCode-org/site&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;HagiCode official site: &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;hagicode.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this article helped you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give a Star on GitHub: &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;github.com/HagiCode-org/site&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Visit the official site to learn more: &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;hagicode.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Watch the official demo video: &lt;a href="https://www.bilibili.com/video/BV1z4oWB3EpY/" rel="noopener noreferrer"&gt;www.bilibili.com/video/BV1z4oWB3EpY/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;One-click installation experience: &lt;a href="https://docs.hagicode.com/installation/docker-compose" rel="noopener noreferrer"&gt;docs.hagicode.com/installation/docker-compose&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Desktop quick installation: &lt;a href="https://hagicode.com/desktop/" rel="noopener noreferrer"&gt;hagicode.com/desktop/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Original Article &amp;amp; License
&lt;/h2&gt;

&lt;p&gt;Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.&lt;br&gt;
This article was created with AI assistance and reviewed by the author before publication.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author: &lt;a href="https://www.newbe.pro" rel="noopener noreferrer"&gt;newbe36524&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Original URL: &lt;a href="https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-05-09-steamworks-multilingual-metadata-management%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-05-09-steamworks-multilingual-metadata-management%2F&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>steamworks</category>
      <category>bbcode</category>
    </item>
    <item>
      <title>Quantifying AI Cost-Benefit Analysis</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Sat, 09 May 2026 02:20:13 +0000</pubDate>
      <link>https://dev.to/newbe36524/quantifying-ai-cost-benefit-analysis-8ka</link>
      <guid>https://dev.to/newbe36524/quantifying-ai-cost-benefit-analysis-8ka</guid>
      <description>&lt;h1&gt;
  
  
  Quantifying AI Cost-Benefit Analysis
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Your boss asks: "How much does it cost to equip employees with AI assistants, and is it worth it?" You can't answer, and you feel unsure. This article discusses how to calculate this clearly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;In recent years, Claude Code, GitHub Copilot, and various AI programming assistants have flooded in like a tidal wave. As a technical person, you've probably already started using them and feel genuinely more efficient—like someone handing you a ladder when you need to climb.&lt;/p&gt;

&lt;p&gt;But when it comes to discussing ROI with your boss or clients, you often hit a wall—how do you quantify that subjective feeling of "increased efficiency"? I understand this feeling. It's like when someone asks you "what do you like about her?" and you stutter for a while, only saying "I just do." That's fine, but bosses want numbers, not your feelings.&lt;/p&gt;

&lt;p&gt;And that's not the only problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ROI&lt;/strong&gt;: Is the cost of equipping the team with AI tools worth it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Efficiency Quantification&lt;/strong&gt;: How do we translate "productivity gains" across different roles and usage levels into measurable metrics?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk Assessment&lt;/strong&gt;: If competitors大规模 adopt AI, how much will our competitiveness suffer?&lt;/p&gt;

&lt;p&gt;Traditional ROI calculations often overlook two critical factors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise Total Cost Perspective&lt;/strong&gt;: Only considering salary while ignoring city differences, social insurance, housing fund, and other additional costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Economics Model&lt;/strong&gt;: Lack of a calculation framework connecting AI usage (Tokens) to actual output&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both factors are indispensable. Here's a real example: For the same 300k annual salary, the actual cost to the enterprise in Beijing versus Wuhan can differ by over 30%. And that's not even counting the cost of AI usage itself. Cost is like an iceberg—you only ever see the tip...&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution shared in this article comes from our practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project.&lt;/p&gt;

&lt;p&gt;HagiCode is essentially just an AI code assistant project. However, during development, we genuinely needed to accurately assess the cost-effectiveness of different AI models—after all, money doesn't grow on trees. To that end, we built a complete calculation framework and open-sourced the &lt;a href="https://cost.hagicode.com" rel="noopener noreferrer"&gt;HagiCode Cost&lt;/a&gt; assessment tool.&lt;/p&gt;

&lt;p&gt;If you're also thinking about AI cost issues, this approach might give you some reference. Or maybe not—I can't guarantee that, but we're just giving it a try.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Calculation Framework
&lt;/h2&gt;

&lt;p&gt;A complete AI cost-benefit assessment requires establishing a three-layer model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input Layer
├── Annual salary data
├── City tier coefficient
├── AI model selection
├── Efficiency multiplier estimate
└── Daily Token usage

Calculation Layer
├── Enterprise total cost accounting
├── AI annual cost calculation
├── Cost proportion analysis
├── ROI calculation
└── Equivalent workforce conversion

Output Layer
├── AI cost proportion
├── Efficiency gain
├── Return on investment
├── Equivalent workforce count
└── Elimination risk assessment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This framework looks complex enough to make your head spin. Actually, the core logic is quite simple: calculate the enterprise's real labor costs clearly, calculate the AI's annual costs clearly, then look at the ROI and equivalent workforce. After all, simplifying complexity is the right path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculating Key Metrics
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Enterprise Annual Total Labor Cost
&lt;/h3&gt;

&lt;p&gt;First, enterprise total cost—this isn't simply annual salary multiplied by 12 months. Real costs need to consider two factors:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;City Coefficient&lt;/strong&gt;: Additional costs in first-tier cities (Beijing, Shanghai, Guangzhou, Shenzhen) are about 30% higher than other cities. This includes social insurance, housing fund, various benefits, and the cost-of-living premium for first-tier cities—after all, the price of living in Beijing versus Wuhan is indeed different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Additional Employment Costs&lt;/strong&gt;: Roughly equivalent to 1 month's salary, covering year-end bonuses, various subsidies, office equipment amortization, etc. These amounts may seem small individually, but they add up.&lt;/p&gt;

&lt;p&gt;So the formula is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;enterpriseAnnualTotalLaborCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;annualSalary&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;cityCoefficient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;annualSalary&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;City coefficient can refer to this standard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First-tier cities (Beijing, Shanghai, Guangzhou, Shenzhen): 0.4&lt;/li&gt;
&lt;li&gt;New first-tier (Hangzhou, Chengdu, Suzhou, Nanjing): 0.3&lt;/li&gt;
&lt;li&gt;Second-tier cities (Wuhan, Xi'an, Tianjin, Zhengzhou): 0.2&lt;/li&gt;
&lt;li&gt;Other cities: 0.1&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AI Annual Cost
&lt;/h3&gt;

&lt;p&gt;AI cost calculation is slightly more complex because AI models charge by Token. And input and output prices differ—output is typically 5-10x more expensive than input. This isn't surprising, after all, output is the AI "working," while input is just you "talking."&lt;/p&gt;

&lt;p&gt;In code scenarios, the input-output ratio is about 3:1, so we can calculate a composite unit price:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Composite unit price (based on 3:1 input-output ratio)&lt;/span&gt;
&lt;span class="nx"&gt;compositeUnitPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="nx"&gt;inputPrice&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;outputPrice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;

&lt;span class="c1"&gt;// Daily cost&lt;/span&gt;
&lt;span class="nx"&gt;dailyAICost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dailyTokenUsage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;M&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="nx"&gt;compositeUnitPrice&lt;/span&gt;

&lt;span class="c1"&gt;// Annual cost (based on 264 working days)&lt;/span&gt;
&lt;span class="nx"&gt;annualAICost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dailyAICost&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mi"&gt;264&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, GPT-5.4's input price is 2.5 USD/1M Token, output price is 15 USD/1M Token. Then the composite unit price is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;compositeUnitPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;5.625&lt;/span&gt; &lt;span class="nx"&gt;USD&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;M&lt;/span&gt; &lt;span class="nx"&gt;Token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Converting to RMB (assuming 1 USD = 7.25 CNY):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;compositeUnitPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;5.625&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mf"&gt;7.25&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;40.78&lt;/span&gt; &lt;span class="nx"&gt;yuan&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;M&lt;/span&gt; &lt;span class="nx"&gt;Token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exchange rates fluctuate, but we fix them for calculation convenience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Benefit Metrics
&lt;/h3&gt;

&lt;p&gt;With the two costs above, we can calculate core metrics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AI cost proportion&lt;/span&gt;
&lt;span class="nx"&gt;aiCostProportion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;annualAICost&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;enterpriseAnnualTotalLaborCost&lt;/span&gt;

&lt;span class="c1"&gt;// Efficiency gain&lt;/span&gt;
&lt;span class="nx"&gt;efficiencyGain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;efficiencyMultiplier&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;// AI return on investment&lt;/span&gt;
&lt;span class="nx"&gt;aiROI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;efficiencyGain&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;aiCostProportion&lt;/span&gt;

&lt;span class="c1"&gt;// Affordable workflow count&lt;/span&gt;
&lt;span class="nx"&gt;affordableCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;enterpriseAnnualTotalLaborCost&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;annualAICost&lt;/span&gt;

&lt;span class="c1"&gt;// Equivalent workforce&lt;/span&gt;
&lt;span class="nx"&gt;equivalentWorkforce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;efficiencyMultiplier&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;affordableCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meanings of these metrics:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Cost Proportion&lt;/strong&gt;: The percentage of enterprise labor costs consumed to maintain Agent workflows. The lower this number, the more "cost-effective" the AI usage. Who doesn't like saving money?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Return on Investment&lt;/strong&gt;: Efficiency gain ÷ AI cost proportion. Less than 1 means "somewhat wasteful," greater than 2 means "very worthwhile." This is easy to understand—like spending money to buy time, you calculate whether it's worth it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Equivalent Workforce&lt;/strong&gt;: There's a point easily misunderstood here. It's not directly accepting the efficiency multiplier, but whether the enterprise can afford this AI workflow. If affordableCount is less than 1, then equivalent workforce won't reach your expected efficiency multiplier. After all, even the cleverest housewife can't cook without rice...&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Calculation Example
&lt;/h2&gt;

&lt;p&gt;Let's do a real accounting example. Assume a first-tier city backend developer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Annual salary: 300k&lt;/li&gt;
&lt;li&gt;Using GPT-5.4, efficiency multiplier: 2.5x&lt;/li&gt;
&lt;li&gt;Daily Token usage: 12 M&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Calculate enterprise total cost&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;enterpriseTotalCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;44.5&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Calculate AI annual cost&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;compositeUnitPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;40.78&lt;/span&gt; &lt;span class="nx"&gt;yuan&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;M&lt;/span&gt; &lt;span class="nx"&gt;Token&lt;/span&gt;
&lt;span class="nx"&gt;dailyCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mf"&gt;40.78&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;489.36&lt;/span&gt; &lt;span class="nx"&gt;yuan&lt;/span&gt;
&lt;span class="nx"&gt;annualCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;489.36&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mi"&gt;264&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;129&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;191&lt;/span&gt; &lt;span class="nx"&gt;yuan&lt;/span&gt; &lt;span class="err"&gt;≈&lt;/span&gt; &lt;span class="mf"&gt;12.9&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Calculate benefit metrics&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;AI&lt;/span&gt; &lt;span class="nx"&gt;cost&lt;/span&gt; &lt;span class="nx"&gt;proportion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;12.9&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;44.5&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;
&lt;span class="nx"&gt;efficiencyGain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt; &lt;span class="nx"&gt;investment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;0.29&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;5.17&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Calculate equivalent workforce&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;affordableCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;44.5&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;12.9&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;3.45&lt;/span&gt;
&lt;span class="nx"&gt;equivalentWorkforce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.5&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt; &lt;span class="nx"&gt;people&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's the conclusion? This AI usage has an ROI over 5, falling in the "very worthwhile" range. If the entire team uses it, forming approximately 2.5 people's production capacity advantage, it would be very competitive in the market.&lt;/p&gt;

&lt;p&gt;This makes sense—after all, the money you spend on AI is far less than your additional output. This deal is worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Impact of Multi-Agent
&lt;/h2&gt;

&lt;p&gt;HagiCode discovered an interesting phenomenon in actual use: a single Agent's efficiency gains have an upper limit.&lt;/p&gt;

&lt;p&gt;This is actually quite natural—no matter how capable a person is, they can only do one thing at a time. After all, you're not an octopus.&lt;/p&gt;

&lt;p&gt;Traditional single Agent usage patterns have several bottlenecks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Serial Limitation&lt;/strong&gt;: Proposal → Implementation → Review → Fix must wait sequentially. No matter how fast a single Agent is, it can only do one thing at a time. It's like cooking—you can only wash, cut, and stir-fry step by step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quota Waste&lt;/strong&gt;: Monthly quota limits can't be fully utilized. Unused quota this month doesn't roll over to next month. This isn't surprising, just a bit wasteful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context Switching&lt;/strong&gt;: Different tasks require repeatedly establishing context, meaning you have to explain background information each time. Like chatting with different people about the same thing—starting from scratch each time gets tiring.&lt;/p&gt;

&lt;p&gt;HagiCode's multi-Agent architecture solves these problems through parallel sessions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parallel 10x+&lt;/strong&gt;: Multiple Agents drive multiple instances simultaneously, achieving true parallel work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throughput Increase&lt;/strong&gt;: Proposal, implementation, and fixes can advance in parallel without waiting for each other&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improved Token Utilization&lt;/strong&gt;: OpenSpec process reduces rework, spreading equivalent consumption&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The change this brings is enormous. Using the previous example, if using HagiCode multi-Agent architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parallel sessions: 4&lt;/li&gt;
&lt;li&gt;Token utilization improvement: 1.5x&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Amplified calculation&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;amplifiedEfficiency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;
&lt;span class="nx"&gt;optimizedDailyToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="nx"&gt;M&lt;/span&gt;
&lt;span class="nx"&gt;optimizedAnnualCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mf"&gt;40.78&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mi"&gt;264&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;344&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;New benefit metrics&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;newAICostProportion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;34.4&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;44.5&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;
&lt;span class="nx"&gt;newROI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;0.77&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;11.68&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;
&lt;span class="nx"&gt;newEquivalentWorkforce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="nx"&gt;people&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although AI cost proportion rose from 29% to 77%, ROI increased from 5.17x to 11.68x, and equivalent workforce changed from 2.5 to 10 people.&lt;/p&gt;

&lt;p&gt;This is the power of multi-Agent parallelism. One Agent is one person; ten Agents are a team... The difference isn't just a little bit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Don't Get City Coefficient Wrong
&lt;/h3&gt;

&lt;p&gt;Employment cost differences across cities are significant—first-tier cities' additional costs are about 30% higher than other cities. When calculating, be sure to use the correct city tier. A small difference in this number can significantly skew the final result. After all, "a miss is as good as a mile"... This is an old saying, but it still holds true.&lt;/p&gt;

&lt;h3&gt;
  
  
  Input-Output Ratio Isn't Fixed
&lt;/h3&gt;

&lt;p&gt;Code scenarios default to a 3:1 input-output ratio, matching the proportion of prompts to generated code in actual programming. But if you're doing other types of work—like writing copy or doing data analysis—this ratio might be completely different.&lt;/p&gt;

&lt;p&gt;This is normal—different work, different methods.&lt;/p&gt;

&lt;h3&gt;
  
  
  Efficiency Multiplier Is Subjective
&lt;/h3&gt;

&lt;p&gt;Efficiency multiplier is a subjective estimate. It's recommended to combine with actual observation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1.5-2x: Familiar with basic functions, occasional use&lt;/li&gt;
&lt;li&gt;2-3x: Proficient, daily high-frequency use&lt;/li&gt;
&lt;li&gt;3x+: Deep integration, forming专属 workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't estimate too high initially—observe for a while before adjusting. After all, higher expectations lead to greater disappointment.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to Calculate Token Usage
&lt;/h3&gt;

&lt;p&gt;If you don't know your daily Token usage, you can estimate this way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check platform usage statistics (both Claude and OpenAI have them)&lt;/li&gt;
&lt;li&gt;Record Token consumption from several typical conversations and take an average&lt;/li&gt;
&lt;li&gt;Multiply by your daily conversation count&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or just use &lt;a href="https://cost.hagicode.com" rel="noopener noreferrer"&gt;HagiCode Cost&lt;/a&gt; to calculate—it has reference values for common scenarios. This is convenient and saves you from blind trial and error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Impact of Exchange Rate Fluctuations
&lt;/h3&gt;

&lt;p&gt;USD models require exchange rate conversion, but rates fluctuate. Calculators typically use fixed rates (like 1 USD = 7.25 CNY), while actual costs may vary with exchange rate fluctuations. This error is usually small, but keep it in mind.&lt;/p&gt;

&lt;p&gt;After all, everything has an approximation—precision to several decimal places isn't really necessary...&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Implementation Points
&lt;/h2&gt;

&lt;p&gt;If you want to implement this calculation logic yourself, several technical details are worth noting:&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Currency Support
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;convertCnyAmountToCurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;amountCny&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;targetCurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CNY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetCurrency&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CNY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;amountCny&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;amountCny&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;EXCHANGE_RATE_USD_TO_CNY&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's not much to say about this code—it's just simple currency conversion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Language Localization
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getLocalizedModelCopy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ModelPricing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SupportedLanguage&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;LocalizedModelMeta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;descriptionEn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pricingContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pricingContext&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pricingContextEn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other fields&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multi-language support is complex in some ways, simple in others. It's essentially storing different language content and retrieving it when needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regional Differentiation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getCityTierLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;cityTier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CityTier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cn-mainland&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;international&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SupportedLanguage&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;benchmarkData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cityCoefficients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;cityTier&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cn-mainland&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;labelEn&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internationalLabel&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internationalLabelEn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Regional differentiation means displaying different labels for different regions. This isn't difficult—just judge the region and language, then return the corresponding value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;AI cost-benefit assessment isn't anything profound—the core is three calculations: enterprise labor costs, AI usage costs, and efficiency improvement magnitude. Calculate these three clearly, and the ROI naturally emerges.&lt;/p&gt;

&lt;p&gt;This is like many things in life—seemingly complex, but when broken down, it's just that. Few people are willing to sit down and calculate it.&lt;/p&gt;

&lt;p&gt;But there's an easily overlooked point here: the multiplier effect from multi-Agent architecture. No matter how strong a single Agent is, it can only improve efficiency linearly. But multiple Agents working in parallel bring exponential capacity improvements. This is the core reason HagiCode chose a multi-Agent architecture.&lt;/p&gt;

&lt;p&gt;One person's power is limited; a group's power is infinite. This sounds like a platitude, but applied to AI, it's fitting.&lt;/p&gt;

&lt;p&gt;If you're also thinking about AI cost issues, welcome to try &lt;a href="https://cost.hagicode.com" rel="noopener noreferrer"&gt;HagiCode Cost&lt;/a&gt; to experience our calculator. Or go directly to &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; to see the source code—maybe it'll give you some inspiration.&lt;/p&gt;

&lt;p&gt;Or maybe not—I can't guarantee that. Just giving it a try, after all, paths are made by walking...&lt;/p&gt;




&lt;p&gt;Writing this, I suddenly remembered an old saying: &lt;strong&gt;"To do good work, one must first sharpen one's tools."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But sometimes, even with sharp tools, knowing how to use them is another matter. AI is like a double-edged sword—used well, it's assistance; used poorly, it's a burden. The balance is for you to find.&lt;/p&gt;

&lt;p&gt;Enough of that. Hope this helps you.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cost.hagicode.com" rel="noopener noreferrer"&gt;HagiCode Cost - AI Cost Assessment Tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode Official Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openai.com/api/pricing/" rel="noopener noreferrer"&gt;OpenAI Official Pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.anthropic.com/en/docs/about-claude/pricing" rel="noopener noreferrer"&gt;Anthropic Claude Pricing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>hagicode</category>
      <category>roi</category>
    </item>
    <item>
      <title>Optimizing OpenSpec Phase Efficiency with Different Agents: HagiCode Practice Summary</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Fri, 08 May 2026 06:24:01 +0000</pubDate>
      <link>https://dev.to/newbe36524/optimizing-openspec-phase-efficiency-with-different-agents-hagicode-practice-summary-1nhb</link>
      <guid>https://dev.to/newbe36524/optimizing-openspec-phase-efficiency-with-different-agents-hagicode-practice-summary-1nhb</guid>
      <description>&lt;h1&gt;
  
  
  Optimizing OpenSpec Phase Efficiency with Different Agents: HagiCode Practice Summary
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Generic prompts cannot handle the specific requirements of different development stages. Through phase-specific agents and a parameterized template system, AI can produce high-quality output at every step.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;OpenSpec is a proposal-driven development system that manages the creation, review, and implementation of technical proposals through structured workflows. The idea itself is sound, but in practice, we found significant issues with using a single generic AI prompt.&lt;/p&gt;

&lt;p&gt;The explore stage lacks context anchoring, causing AI explorations to deviate from the proposal scope; artifact generation quality is unstable, with design.md missing visual elements, proposal.md lacking code change tables, and tasks.md even including Git operations that shouldn't be there; responsibility boundaries are blurred, with unclear content requirements for different document types; prompts lack flexibility, unable to dynamically adjust AI behavior based on different scenarios.&lt;/p&gt;

&lt;p&gt;These issues directly impact the efficiency and output quality of the OpenSpec workflow. There's really no other way but to modify the prompt templates ourselves. This article documents that period of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution shared in this article comes from our practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode is an AI-powered code assistant, and we extensively use the OpenSpec workflow to manage technical proposals during development. The agent layering strategy introduced here is exactly the optimization solution we summarized from practical use.&lt;/p&gt;

&lt;p&gt;If you find this approach valuable, it means our engineering practices are pretty solid—HagiCode itself is worth paying attention to.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenSpec Workflow Analysis
&lt;/h2&gt;

&lt;p&gt;The OpenSpec system contains multiple core stages, each with specific goals and constraints. Understanding the responsibility boundaries of these stages is the foundation for designing effective agent strategies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────────┐
│                    OpenSpec Workflow Stages                         │
├─────────────────────────────────────────────────────────────────────┤
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐     │
│  │ Explore  │ -&amp;gt; │   New    │ -&amp;gt; │    FF    │ -&amp;gt; │  Apply   │     │
│  └──────────┘    └──────────┘    └──────────┘    └──────────┘     │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐     │
│  │ Archive  │    │   Sync   │    │ Verify   │    │  Status  │     │
│  └──────────┘    └──────────┘    └──────────┘    └──────────┘     │
└─────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each stage has completely different goals: The Explore stage requires a thinking posture, focused on information gathering; the New stage focuses on requirement analysis and solution design; the FF stage creates artifacts in batch by dependency order; the Apply stage transforms proposals into actual code. Using the same prompt template to drive these vastly different tasks is clearly unreasonable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt System Architecture
&lt;/h2&gt;

&lt;p&gt;OpenSpec uses a templated prompt system, which provides the technical foundation for agent layering. Template files use &lt;code&gt;.hbs&lt;/code&gt; (Handlebars/Scriban) format, paired with &lt;code&gt;.json&lt;/code&gt; metadata files to define parameters and validation rules, supporting both Chinese and English.&lt;/p&gt;

&lt;p&gt;The key design is the &lt;code&gt;PromptScenario&lt;/code&gt; enumeration, which defines prompt scenarios for different stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;PromptScenario&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;OpenspecV1Explore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// Exploration stage&lt;/span&gt;
    &lt;span class="n"&gt;OpenspecV1New&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// New proposal&lt;/span&gt;
    &lt;span class="n"&gt;OpenspecV1Ff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// Fast generation&lt;/span&gt;
    &lt;span class="n"&gt;OpenspecV1Apply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// Apply changes&lt;/span&gt;
    &lt;span class="n"&gt;OpenspecV1Archive&lt;/span&gt;       &lt;span class="c1"&gt;// Archive&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each scenario has a corresponding independent template file, such as &lt;code&gt;openspec-v1-explore.zh-CN.hbs&lt;/code&gt; and &lt;code&gt;openspec-v1-ff.zh-CN.hbs&lt;/code&gt;, allowing specific constraints and guidance to be injected for different stages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parameterized Prompt Loading
&lt;/h2&gt;

&lt;p&gt;Implementing dynamic parameter injection is the core of the entire system. &lt;code&gt;FilePromptProvider&lt;/code&gt; is responsible for loading prompts based on scenarios and parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetOpenspecV1FfPromptAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;changeName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;changeDescription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"en-US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;planningDirectionInstructions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"planningDirectionInstructions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
          &lt;span class="nf"&gt;ResolvePlanningDirectionInstructions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;planningDirectionInstructions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;changeName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"changeName"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;changeName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;GetPromptWithParametersAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;PromptScenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OpenspecV1Ff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design allows us to dynamically inject parameters at runtime, such as &lt;code&gt;changeName&lt;/code&gt; and &lt;code&gt;planningDirectionInstructions&lt;/code&gt;, without modifying the template file itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamic Planning Direction Configuration
&lt;/h2&gt;

&lt;p&gt;HagiCode implements a flexible planning direction system that allows users to select different directions for each generation. Each direction has an independent ID, description, and prompt fragment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProposalPlanningDirections&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ProposalPlanningDirectionDefinition&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;Catalog&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;ExploreId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"Explore mode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;DefaultEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;EnglishPromptFragment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="s"&gt;"- Explore mode: add an explicit exploration pass..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ChinesePromptFragment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="s"&gt;"- 探索模式：在定稿工件之前增加明确的探索阶段..."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="c1"&gt;// ... change-map, flowchart, prototype, architecture, sequence&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;NormalizedProposalPlanningDirections&lt;/span&gt; &lt;span class="nf"&gt;Normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;enableExploreMode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PlanningDirectionOptionDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;planningDirections&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Merge default configuration with user custom configuration&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supported directions include: &lt;code&gt;explore&lt;/code&gt; (exploration mode), &lt;code&gt;change-map&lt;/code&gt; (change map), &lt;code&gt;flowchart&lt;/code&gt; (interaction flowchart), &lt;code&gt;prototype&lt;/code&gt; (UI prototype), &lt;code&gt;architecture&lt;/code&gt; (architecture diagram), &lt;code&gt;sequence&lt;/code&gt; (API sequence diagram). Users can freely toggle these directions, and the system dynamically generates corresponding prompt instruction blocks.&lt;/p&gt;

&lt;p&gt;Use conditional statements in Handlebars templates to inject these instructions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="k"&gt;{{#if&lt;/span&gt; &lt;span class="nv"&gt;planningDirectionInstructions&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
## Planning Directions for This Generation

&lt;span class="k"&gt;{{{&lt;/span&gt;&lt;span class="nv"&gt;planningDirectionInstructions&lt;/span&gt;&lt;span class="k"&gt;}}}&lt;/span&gt;
&lt;span class="k"&gt;{{/if}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Clear Content Scope Constraints
&lt;/h2&gt;

&lt;p&gt;The most critical improvement is defining clear content scope constraints for different document types, especially tasks.md. We added strict constraint conditions in the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### tasks.md Content Scope Constraints&lt;/span&gt;

When creating &lt;span class="sb"&gt;`tasks.md`&lt;/span&gt; artifacts, the following content scope constraints MUST be observed:

&lt;span class="gs"&gt;**MUST include**&lt;/span&gt;:
&lt;span class="p"&gt;-&lt;/span&gt; Business logic tasks (code implementation, feature development)
&lt;span class="p"&gt;-&lt;/span&gt; Technical implementation tasks (component integration, API development)
&lt;span class="p"&gt;-&lt;/span&gt; Testing tasks (unit tests, integration tests)
&lt;span class="p"&gt;-&lt;/span&gt; Documentation tasks (updating documentation, adding comments)

&lt;span class="gs"&gt;**MUST NOT include**&lt;/span&gt;:
&lt;span class="p"&gt;-&lt;/span&gt; Git commit operations (git add, git commit, git push)
&lt;span class="p"&gt;-&lt;/span&gt; Version control management workflows
&lt;span class="p"&gt;-&lt;/span&gt; Deployment and release operations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using normative language (MUST/SHALL) rather than suggestive language ensures AI strictly understands these constraints. For proposal.md and design.md, we also clarified their respective responsibility boundaries: proposal.md must include code change tables and UI prototype diagrams (when involving UI changes), while design.md must include architecture diagrams and data flow diagrams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exploration Stage Context Anchoring
&lt;/h2&gt;

&lt;p&gt;The Explore stage problem is easily overlooked—AI explorations may completely deviate from the proposal scope. We address this through enhanced prompts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Explore Execution Principles&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**No documentation needed**&lt;/span&gt; - Exploration results need not be saved as independent documents
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Information transfer**&lt;/span&gt; - After exploration is complete, collected information will be passed to the Proposal creation stage
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Focus on thinking**&lt;/span&gt; - The value of exploration lies in information gathering, not document output

&lt;span class="gu"&gt;## Connection with Proposal Creation&lt;/span&gt;

The Explore stage occurs after proposal creation and before project code is written. After exploration is complete,
the system will guide you to create or populate the &lt;span class="sb"&gt;`proposal.md`&lt;/span&gt; file, and exploration-collected information will serve as the foundation for proposal content.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This clarifies the positioning of the Explore stage: it's a preliminary step for information gathering, not an independent document production phase. Once AI understands this, it can focus more on proposal-related knowledge exploration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Guide
&lt;/h2&gt;

&lt;p&gt;If you want to apply this solution in HagiCode, follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Define planning directions&lt;/strong&gt;: Define direction IDs, default states, and prompt fragments in &lt;code&gt;ProposalPlanningDirections.cs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Template parameterization&lt;/strong&gt;: Use conditional statements and variable injection in &lt;code&gt;.hbs&lt;/code&gt; templates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify output&lt;/strong&gt;: When enabling specific directions, check that corresponding artifacts contain expected content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test boundaries&lt;/strong&gt;: Verify that disabling directions doesn't generate corresponding content and doesn't affect other directions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note that template modifications should remain synchronized with upstream, and Chinese and English template structures should be consistent. Planning direction rendering should complete in microseconds to avoid performance impact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The efficiency optimization of the OpenSpec workflow lies in understanding the differentiated needs of different stages. Through stage-specific agents, parameterized templates, and clear content constraints, we enable AI to produce high-quality output at every step.&lt;/p&gt;

&lt;p&gt;This solution has been validated in HagiCode's practice—not only improving document quality but also reducing manual modification workload. If your team is also using a similar proposal-driven workflow, I hope these experiences provide some inspiration.&lt;/p&gt;

&lt;p&gt;It's really just about breaking down the problem. Each stage has its characteristics, use the right method, and the problem naturally becomes simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;HagiCode project repository: &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;github.com/HagiCode-org/site&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;HagiCode official website: &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;hagicode.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Official version demo video: &lt;a href="https://www.bilibili.com/video/BV1z4oWB3EpY/" rel="noopener noreferrer"&gt;www.bilibili.com/video/BV1z4oWB3EpY/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;One-click installation experience: &lt;a href="https://docs.hagicode.com/installation/docker-compose" rel="noopener noreferrer"&gt;docs.hagicode.com/installation/docker-compose&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Desktop quick installation: &lt;a href="https://hagicode.com/desktop/" rel="noopener noreferrer"&gt;hagicode.com/desktop/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If this article helps you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give it a like to help more people see it&lt;/li&gt;
&lt;li&gt;Come to GitHub and give us a Star&lt;/li&gt;
&lt;li&gt;Visit the official website to learn more&lt;/li&gt;
&lt;li&gt;Watch the demo video to understand complete features&lt;/li&gt;
&lt;li&gt;One-click installation to start experiencing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Public beta has begun, welcome to install and experience!&lt;/p&gt;

</description>
      <category>openspec</category>
      <category>ai</category>
      <category>agents</category>
      <category>hagicode</category>
    </item>
    <item>
      <title>Desktop Application P2P Distribution Acceleration Practice: Full-Chain Integration from Consumer to Publisher</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Fri, 08 May 2026 01:23:10 +0000</pubDate>
      <link>https://dev.to/newbe36524/desktop-application-p2p-distribution-acceleration-practice-full-chain-integration-from-consumer-to-45j6</link>
      <guid>https://dev.to/newbe36524/desktop-application-p2p-distribution-acceleration-practice-full-chain-integration-from-consumer-to-45j6</guid>
      <description>&lt;h1&gt;
  
  
  Desktop Application P2P Distribution Acceleration Practice: Full-Chain Integration from Consumer to Publisher
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Large file distribution for desktop applications has always been a headache—high bandwidth costs, slow download speeds, and poor user experience. This article shares the hybrid distribution solution we implemented in HagiCode Desktop, which accelerates downloads through P2P technology while maintaining HTTP fallback capability, ultimately achieving a complete closed loop between the publisher and consumer sides.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;Desktop application distribution packages are typically not small, often running into hundreds of MB. This is actually quite normal—after all, modern applications have more and more features, so naturally their size increases. For applications like HagiCode Desktop, each version update means distributing large files to a large number of users, which poses a significant challenge to server bandwidth.&lt;/p&gt;

&lt;p&gt;The traditional approach is direct HTTP download—simple and straightforward, but the problems are obvious: high server load during peak periods, slow download speeds for users, especially overseas users. There's really no way around this, as physical distance is what it is. P2P technology can solve this problem well—users share file fragments with each other, reducing server pressure while improving download speeds.&lt;/p&gt;

&lt;p&gt;But things aren't that simple. During the development of HagiCode Desktop, we discovered an interesting phenomenon: the consumer side (desktop application) already had hybrid download capabilities, able to parse fields like &lt;code&gt;torrentUrl&lt;/code&gt;, &lt;code&gt;infoHash&lt;/code&gt;, &lt;code&gt;webSeeds&lt;/code&gt;, &lt;code&gt;sha256&lt;/code&gt;, etc., and prioritize P2P-accelerated downloads through a hybrid download coordinator. However, the publisher side (build toolchain) didn't stably output these fields to Azure Blob's &lt;code&gt;index.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This actually created a disconnect: the client was expecting a more efficient distribution method, but the publisher was still using the traditional flat file list to build the index. The potential of P2P acceleration was being wasted, which is a shame.&lt;/p&gt;

&lt;p&gt;To close this loop, we implemented a complete overhaul solution—from metadata generation on the publisher side to hybrid download coordination on the consumer side, making the entire distribution chain truly work. Next, I'll share in detail the design thinking and implementation details of this solution, hoping to provide some reference for friends facing similar problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The hybrid distribution solution shared in this article comes from our practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode Desktop is our desktop application, supporting Windows, macOS, and Linux platforms. As an AI code assistant project, the desktop client needs to update distribution packages frequently, which prompted us to explore more efficient distribution methods. After all, no one wants to wait half a day for every update, right?&lt;/p&gt;

&lt;h2&gt;
  
  
  Analysis
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Nature of the Problem
&lt;/h3&gt;

&lt;p&gt;On the surface, this looks like a "add torrent file generation" feature requirement. But upon deeper analysis, we discovered this is actually a &lt;strong&gt;producer-consumer contract mismatch&lt;/strong&gt; problem. This kind of situation is quite common—sometimes development and operations just aren't on the same page.&lt;/p&gt;

&lt;p&gt;The consumer side expects asset-level hybrid distribution fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"torrentUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"infoHash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;sha1 infohash&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"webSeeds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://..."&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sha256"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;package digest&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the publisher side provides a file-level flat list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hagicode-1.2.3-win-x64.zip"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://..."&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hagicode-1.2.3-win-x64.zip.torrent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://..."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These two are semantically completely mismatched. The consumer side cannot determine from a flat list which file is the main file and which is the sidecar, nor can it establish associations between them. It's like trying to find someone, but only being given a phonebook and told to find them yourself—quite troublesome.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Constraints
&lt;/h3&gt;

&lt;p&gt;When designing the solution, we defined several constraints that must be met:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Threshold Consistency&lt;/strong&gt;: The publisher and consumer must use the same file size threshold. We set it to 100 MB—only files reaching this size generate P2P metadata. This avoids policy drift where "the publisher marks it as acceleratable but the consumer decides not to accelerate." This is actually quite important, after all, if the two sides are inconsistent, various strange bugs will appear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fallback Guarantee&lt;/strong&gt;: &lt;code&gt;webSeeds&lt;/code&gt; must include &lt;code&gt;directUrl&lt;/code&gt;. This ensures that even without P2P connections (for example, as the first downloader), users can still download the complete file via HTTP. P2P is an acceleration means, not a replacement. It's like driving—P2P is the highway, but you also need to keep regular roads in case the highway is congested.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compatibility Window&lt;/strong&gt;: &lt;code&gt;index.json&lt;/code&gt; needs to output both &lt;code&gt;assets&lt;/code&gt; and &lt;code&gt;files&lt;/code&gt; projections. Old clients may not recognize the &lt;code&gt;assets&lt;/code&gt; field, so &lt;code&gt;files&lt;/code&gt; needs to be retained as a compatibility projection to avoid client interruption due to server upgrades. This is actually quite common, after all, not all users update their clients in a timely manner.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technical Decisions
&lt;/h3&gt;

&lt;p&gt;In terms of specific implementation, we adopted a "&lt;strong&gt;standalone metadata builder + optional Node bridge script&lt;/strong&gt;" architecture, rather than implementing torrent generation directly in &lt;code&gt;AzureBlobAdapter&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This has several benefits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Clear responsibilities&lt;/strong&gt;: Metadata construction logic is independent of the storage adapter, facilitating testing and maintenance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform decoupling&lt;/strong&gt;: C# environment can call Node scripts to generate torrents, leveraging existing torrent libraries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration-friendly&lt;/strong&gt;: If we need to migrate to other storage backends in the future, the metadata builder can be reused&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is actually a pretty good choice, after all, when responsibilities are clear, subsequent maintenance is much easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Metadata Construction Process
&lt;/h3&gt;

&lt;p&gt;The complete metadata construction process looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Packaging complete → Identify large files (≥100MB) → Calculate sha256 → Generate .torrent sidecar 
→ Extract infoHash → Assemble metadata → Upload ZIP + .torrent → Write index.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step has clear responsibilities:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File identification&lt;/strong&gt;: Iterate through build artifacts and filter files with size ≥ 100 MB. This threshold is consistent with the consumer side's &lt;code&gt;HYBRID_THRESHOLD_BYTES&lt;/code&gt;. This is actually quite important, after all, if thresholds are inconsistent, various strange problems will appear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SHA256 calculation&lt;/strong&gt;: Calculate the SHA256 digest of the main file for integrity verification after download. This is a security line of defense, ensuring that files downloaded by users haven't been tampered with. It's like adding a fingerprint to a file—if it's tampered with, it can be discovered in time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Torrent generation&lt;/strong&gt;: Use a Node script to call the torrent library to generate a &lt;code&gt;.torrent&lt;/code&gt; sidecar file. Naming follows the &lt;code&gt;{artifact}.zip.torrent&lt;/code&gt; format for easy reverse lookup of sidecars from ZIP filenames. This is actually a small trick that makes naming more standardized and convenient for subsequent processing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;InfoHash extraction&lt;/strong&gt;: Extract the infoHash (SHA1 format) from the torrent file, which is the unique identifier for resources in the P2P network. It's like everyone's ID number—without this, the P2P network cannot find the corresponding resource.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metadata assembly&lt;/strong&gt;: Assemble &lt;code&gt;directUrl&lt;/code&gt;, &lt;code&gt;torrentUrl&lt;/code&gt;, &lt;code&gt;infoHash&lt;/code&gt;, &lt;code&gt;webSeeds&lt;/code&gt;, &lt;code&gt;sha256&lt;/code&gt; into a complete asset metadata object.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Index Structure Upgrade
&lt;/h3&gt;

&lt;p&gt;Upgrade from a flat &lt;code&gt;files&lt;/code&gt; projection to an asset-level &lt;code&gt;assets&lt;/code&gt; object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"versions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.2.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"assets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hagicode-1.2.3-win-x64.zip"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"directUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://hagicode.blob.core.windows.net/releases/v1.2.3/hagicode-1.2.3-win-x64.zip"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"torrentUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://hagicode.blob.core.windows.net/releases/v1.2.3/hagicode-1.2.3-win-x64.zip.torrent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"infoHash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sha256"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7a8b9c0d1e2f"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"webSeeds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"https://hagicode.blob.core.windows.net/releases/v1.2.3/hagicode-1.2.3-win-x64.zip"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Compatibility&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;projection&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hagicode-1.2.3-win-x64.zip"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://..."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure has several design considerations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dual projections coexist&lt;/strong&gt;: &lt;code&gt;assets&lt;/code&gt; provides complete hybrid distribution metadata, &lt;code&gt;files&lt;/code&gt; provides a simplified compatibility view. New clients prioritize using &lt;code&gt;assets&lt;/code&gt;, old clients fall back to &lt;code&gt;files&lt;/code&gt;. This is actually a compromise, after all, we can't just leave old users behind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebSeeds includes DirectUrl by default&lt;/strong&gt;: Ensures that even without P2P connections, users can still download completely via HTTP. This is a fallback guaranteeing 100% availability. It's like driving—P2P is the highway, but you also need to keep regular roads in case the highway is congested.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clear naming convention&lt;/strong&gt;: The &lt;code&gt;{artifact}.zip.torrent&lt;/code&gt; naming allows the consumer side to automatically discover sidecars without additional configuration. This is actually a small trick that makes naming more standardized and convenient for subsequent processing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Publishing Orchestration
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Build.AzureStorage.cs&lt;/code&gt; orchestrates the complete process through &lt;code&gt;AzureReleasePublishOrchestrator&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orchestrator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AzureReleasePublishOrchestrator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArtifactHybridMetadataBuilder&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// Build hybrid metadata&lt;/span&gt;
    &lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;orchestrator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;downloadedFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;publishOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;UploadIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;MinifyIndexJson&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;EffectiveGitHubRepository&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The orchestrator ensures sidecars are uploaded before the index, and outputs diagnostic information in the summary. This way, if publication fails, you can quickly locate whether it was sidecar generation failure, missing upload, or index write failure. This is actually quite important, after all, if publication fails, being able to quickly locate the problem saves time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Key Code Modules
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Metadata Consumer
&lt;/h4&gt;

&lt;p&gt;The consumer side builds hybrid distribution metadata from asset objects in &lt;code&gt;index.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// http-index-source.ts:418-463&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;buildHybridMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HttpIndexAsset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;directUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;assetKind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VersionAssetKind&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;HybridDistributionMetadata&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;torrentUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolveOptionalUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;torrentUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasTorrentMetadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;torrentUrl&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infoHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// WebSeeds includes directUrl by default, ensuring fallback&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;webSeeds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;legacyWebSeeds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;structuredWebSeeds&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;directUrl&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;webSeeds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;directUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;webSeeds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;directUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;torrentUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;infoHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infoHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;webSeeds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;hasTorrentMetadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;torrentFirst&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hasTorrentMetadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Prioritize P2P&lt;/span&gt;
    &lt;span class="na"&gt;eligible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hasTorrentMetadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key design points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;torrentFirst&lt;/code&gt; flag controls download strategy, prioritizing P2P when torrent metadata is available&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;webSeeds&lt;/code&gt; forcibly includes &lt;code&gt;directUrl&lt;/code&gt;, ensuring fallback capability&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;eligible&lt;/code&gt; field indicates whether this asset supports hybrid distribution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is actually a small trick—through these flag bits, you can flexibly control download strategy.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Hybrid Download Coordinator
&lt;/h4&gt;

&lt;p&gt;The hybrid download coordinator is responsible for executing the actual download logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hybrid-download-coordinator.ts:83-184&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;download&lt;/span&gt;&lt;span class="p"&gt;(...):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HybridDownloadResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;policyEvaluator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useHybrid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Prioritize Torrent engine download&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cachePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Fall back to HTTP/WebSeed when Torrent fails&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downloadViaHttpSources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cachePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;packageSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// HTTP-only mode&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;packageSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downloadPackage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cachePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// sha256 verification ensures integrity&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cachePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Download strategy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Evaluate user settings and network environment to decide whether to enable hybrid mode&lt;/li&gt;
&lt;li&gt;Prioritize attempting Torrent download (P2P)&lt;/li&gt;
&lt;li&gt;Automatically fall back to HTTP/WebSeed on failure&lt;/li&gt;
&lt;li&gt;Use SHA256 to verify integrity after download completion&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This design ensures the best user experience—acceleration when P2P is available, normal download when it's not. This is actually a pretty good strategy, after all, user experience is what matters most.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Publisher Orchestration
&lt;/h4&gt;

&lt;p&gt;The publisher side coordinates the entire process through an orchestrator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Build.AzureStorage.cs:152-168&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orchestrator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AzureReleasePublishOrchestrator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArtifactHybridMetadataBuilder&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;orchestrator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;downloadedFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;publishOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;UploadIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;MinifyIndexJson&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;EffectiveGitHubRepository&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The orchestrator is responsible for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Calling the metadata builder to generate P2P metadata&lt;/li&gt;
&lt;li&gt;Ensuring both main files and sidecars are uploaded to Blob storage&lt;/li&gt;
&lt;li&gt;Updating both &lt;code&gt;assets&lt;/code&gt; and &lt;code&gt;files&lt;/code&gt; projections in &lt;code&gt;index.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Outputting a publication summary containing diagnostic information&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is actually a pretty good architecture—through the orchestrator, the entire process is strung together, making subsequent maintenance convenient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practical Experience
&lt;/h3&gt;

&lt;p&gt;In implementing this solution, we accumulated some practical experience:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Naming conventions are important&lt;/strong&gt;: Using &lt;code&gt;{artifact}.zip.torrent&lt;/code&gt; makes it easy to reverse-lookup sidecars from ZIP files. This convention seems simple, but in actual operation it can save a lot of trouble—the consumer side can automatically discover sidecars without additional configuration. This is actually a small trick that makes naming more standardized and convenient for subsequent processing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure diagnosis must be clear&lt;/strong&gt;: Publication summaries need to clearly distinguish between sidecar generation failure, missing upload, and index write failure. We suffered from this in early versions—after publication failed, we didn't know which step had problems, making troubleshooting very difficult. Now each step has clear error messages, making problem location much faster. This is actually quite important, after all, debugging time is also a cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graceful degradation&lt;/strong&gt;: Assets that don't meet conditions automatically fall back to HTTP-only, not blocking the entire publication. For example, if a file is smaller than 100 MB, or torrent generation fails, no P2P metadata is generated, and it goes directly to HTTP download. This way, even if the P2P link has problems, basic functionality isn't affected. This is actually a pretty good strategy, after all, you can't let one failed feature affect the entire publication process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Threshold validation&lt;/strong&gt;: The publisher threshold must be consistent with the consumer side's &lt;code&gt;HYBRID_THRESHOLD_BYTES&lt;/code&gt;. We define this value as a constant and test consumer-publisher consistency in CI. If inconsistent, the awkward situation of "publisher thinks it can accelerate but consumer decides not to accelerate" will occur. This is actually quite important, after all, if the two sides are inconsistent, various strange problems will appear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SHA256 is the security line&lt;/strong&gt;: No matter the download channel (P2P, HTTP, WebSeed), everything is verified with SHA256 in the end. This is the last line of defense against file tampering and absolutely cannot be omitted. It's like adding a fingerprint to a file—if it's tampered with, it can be discovered in time. When it comes to security issues, you can't be too careful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Large file distribution for desktop applications is a classic challenge, and P2P technology provides an elegant solution. Through this hybrid distribution architecture, HagiCode Desktop achieved several key goals:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lower distribution costs&lt;/strong&gt;: P2P shares server bandwidth pressure, maintaining stable distribution capability even during peak periods. This is actually a pretty good benefit, after all, saving some bandwidth money is good.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Improved user experience&lt;/strong&gt;: Download speeds increase significantly with P2P connections, especially for overseas users. Without P2P connections, normal download via HTTP is still possible, guaranteeing 100% availability. This is actually a pretty good strategy, after all, user experience is what matters most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smooth evolution path&lt;/strong&gt;: Through the dual-projection index design, independent upgrades of server and client are achieved. Old clients are unaffected, new clients gradually enable P2P acceleration. This is actually a pretty good architecture, after all, if you can upgrade smoothly, you won't affect existing users.&lt;/p&gt;

&lt;p&gt;The core idea of this solution is "progressive enhancement"—HTTP is the baseline, P2P is the enhancement. This both guarantees reliability and provides room for performance improvement. This is actually a pretty good philosophy, after all, you can't sacrifice reliability for the sake of performance.&lt;/p&gt;

&lt;p&gt;If you're also working on desktop application distribution, or facing similar large file distribution problems, I hope this solution provides you some inspiration. P2P technology isn't mysterious—the key is designing good contracts between the publisher and consumer sides, making the entire chain work. This is actually pretty good experience, after all, being able to help others is a good thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode Official Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hagicode.com/desktop/" rel="noopener noreferrer"&gt;HagiCode Desktop Installation Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.bittorrent.org/beps/bep_0003.html" rel="noopener noreferrer"&gt;Bittorrent Protocol Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.bittorrent.org/beps/bep_0019.html" rel="noopener noreferrer"&gt;WebSeed Extension Specification (BEP 0019)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If this article helped you, feel free to give a Star on GitHub: &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;github.com/HagiCode-org/site&lt;/a&gt;. HagiCode Desktop public beta has begun, welcome to install and try it! This is actually a nice invitation, after all, one more trial means one more piece of feedback, which is also a good thing.&lt;/p&gt;

</description>
      <category>p2p</category>
      <category>hagicodedesktop</category>
    </item>
    <item>
      <title>Implementing Image Upload and AI Recognition in Chat: A Complete Solution from Design to Implementation</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Thu, 07 May 2026 06:47:44 +0000</pubDate>
      <link>https://dev.to/newbe36524/implementing-image-upload-and-ai-recognition-in-chat-a-complete-solution-from-design-to-4le0</link>
      <guid>https://dev.to/newbe36524/implementing-image-upload-and-ai-recognition-in-chat-a-complete-solution-from-design-to-4le0</guid>
      <description>&lt;h1&gt;
  
  
  Implementing Image Upload and AI Recognition in Chat: A Complete Solution from Design to Implementation
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;In AI interaction systems, how can we enable users to upload images and have AI directly recognize them? I've actually struggled with this question for quite a while, but fortunately, I've gained some insights through the practice at HagiCode. Today, let's discuss this image upload and recognition solution—from custom protocol design to file system storage, to front-end and back-end separated preview. This serves as a complete technical note.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;In this era of AI chat popularity, visual information is actually an important carrier for users to express their intentions. However, most traditional chat systems only support pure text input, which prevents users from directly passing visual context to AI for analysis—a bit regrettable.&lt;/p&gt;

&lt;p&gt;HagiCode also faced similar challenges during development: users couldn't upload images when chatting or creating main opinions, AI couldn't access users' local visual information, and there was a lack of a complete loop from image input, storage, rendering to AI context delivery.&lt;/p&gt;

&lt;p&gt;Actually, these problems aren't a big deal, they just need some time and patience to solve. We designed and implemented a complete image upload and recognition process, enabling Claude and other AIs to directly recognize and analyze user-uploaded screenshots. Next, I'll detail the implementation of this solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution shared in this article comes from our practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode is an open-source AI code assistant project that uses OpenSpec-based workflow design and is committed to providing a smarter code writing experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analysis
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Technical Challenges
&lt;/h3&gt;

&lt;p&gt;Before starting implementation, we need to first clarify the main challenges we face, after all, sharpening the axe before cutting trees doesn't delay the work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-module collaboration&lt;/strong&gt;: Image upload involves multiple modules including frontend UI, upload service, backend API, file storage, message persistence, and AI execution mapping. Each module has its own responsibilities and interfaces, requiring a coordinated overall solution design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage strategy selection&lt;/strong&gt;: Should images be stored in the database or file system? If choosing file system, how should the directory structure be designed? How to integrate with the existing OpenSpec workflow? These all need careful consideration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reference protocol design&lt;/strong&gt;: A standard image reference method is needed that can be both rendered by the frontend and correctly parsed by the AI execution pipeline. Use file paths directly? HTTP URLs? Or design a dedicated protocol?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI capability compatibility&lt;/strong&gt;: Different AI executors have varying degrees of multimodal support. Some executors natively support image input, while others can only process text. How to design a unified adaptation layer to ensure all executors can correctly handle image information?&lt;/p&gt;

&lt;h3&gt;
  
  
  Design Decisions
&lt;/h3&gt;

&lt;p&gt;After thorough discussion and consideration, we made the following key design decisions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision 1: File System Storage&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We chose to store images in the file system rather than the database. The directory structure is designed as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;system-root&amp;gt;/images/&amp;lt;sessionId&amp;gt;/
├── &amp;lt;timestamp&amp;gt;-&amp;lt;uuid&amp;gt;.jpg
└── &amp;lt;timestamp&amp;gt;-&amp;lt;uuid&amp;gt;.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rationale is quite clear: simplify implementation, avoid database bloat, and files can be directly read by AI. Moreover, image files are essentially not suitable for storage in databases; file system is the more natural choice. It's like putting books on a bookshelf rather than stuffing them into a notebook—same principle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision 2: Custom Protocol &lt;code&gt;hagiimag://&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To avoid conflicts with HTTP URLs while making reference semantics clearer, we designed a custom image reference protocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hagiimag://session-abc123/20260301-143022-a1b2c3d4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This protocol has the format &lt;code&gt;hagiimag://&amp;lt;sessionId&amp;gt;/&amp;lt;imageId&amp;gt;&lt;/code&gt;, with clear semantics and easy to parse and route. Seeing this format, developers can immediately understand it's an image reference, not a regular URL. Such design nuances can sometimes be quite useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision 3: Frontend Preview and AI Access Separation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;During implementation, we discovered that frontend and AI have different access needs for images: the frontend needs to preview through HTTP API, while AI needs to directly read local file paths. Therefore, we designed separated access methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend uses &lt;code&gt;/api/Images/{sessionId}/{imageId}/content&lt;/code&gt; for preview&lt;/li&gt;
&lt;li&gt;AI uses local file paths parsed by the server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ensures both security (not exposing server paths) and usability (browsers can directly access). After all, security and usability always need to be balanced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision 4: Immediate Upload Strategy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Another key decision is the upload timing. We chose to trigger upload immediately when the user selects or pastes an image, only referencing successfully uploaded images when sending messages.&lt;/p&gt;

&lt;p&gt;The benefit is error handling is done upfront, avoiding complexity in the message sending API and maintaining JSON contract simplicity. Users know whether the image upload succeeded before sending, providing better experience. This "prepare for a rainy day" design approach applies in many situations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Architecture Design
&lt;/h3&gt;

&lt;p&gt;Based on the above decisions, we designed the following overall architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend Layer
├── ConversationInputArea  ◄─────── useImageAttachmentManager
│       │                             │
│       ├── File selection            ├── Attachment state management
│       ├── Clipboard paste           ├── Upload/retry/delete
│       └── Attachment preview        └── Image reference generation
│
Service Layer
├── ImageUploadService
│       ├── uploadImage()      ◄─────── ImagesController
│       ├── deleteImage()                 │
│       ├── parseHagiImageUrl()  ◄─────── Parse protocol links
│       └── buildPreviewUrl()              │
│
Backend Layer
├── ImagesController           ◄─────── ImagesDomainService
│       │                                  │
│       ├── POST /upload                  ├── File validation
│       ├── GET /{sessionId}/{imageId}    ├── Image saving
│       ├── DELETE                        ├── Image compression
│       └── GET /content                  └── Reference parsing
│
AI Execution Layer
├── ImageContentBlock          ◄─────── StructuredMessageDomainService
│       │                                  │
│       ├── Multimodal executor           ├── Image block parsing
│       └── Text executor fallback        └── Path hint generation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This architecture clearly shows the complete data flow from frontend to AI. Each layer has clear responsibilities and interacts through standard interfaces. Good architecture is like this—each doing its job, not interfering with each other, smooth communication.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Processes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Image Upload Process&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User selects images through file selection or clipboard paste&lt;/li&gt;
&lt;li&gt;Frontend validates file type and size (supports JPEG/PNG/WEBP/GIF, 10MB per file)&lt;/li&gt;
&lt;li&gt;Calls upload API, image saved to &lt;code&gt;/images/{sessionId}/&lt;/code&gt; directory&lt;/li&gt;
&lt;li&gt;API returns &lt;code&gt;hagiimag://&lt;/code&gt; reference and preview URL&lt;/li&gt;
&lt;li&gt;Frontend displays preview thumbnail in attachment bar, user can preview before sending&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;AI Recognition Process&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User sends message containing image reference&lt;/li&gt;
&lt;li&gt;Backend parses &lt;code&gt;hagiimag://&lt;/code&gt; protocol link, extracts sessionId and imageId&lt;/li&gt;
&lt;li&gt;Maps image reference to &lt;code&gt;ImageContentBlock&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Selects processing method based on executor capability:

&lt;ul&gt;
&lt;li&gt;Multimodal executor: passes structured image input&lt;/li&gt;
&lt;li&gt;Text executor: falls back to image path hint&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This completes a full loop: user uploads image → AI recognizes image → AI returns analysis results. Such smooth processes often bring better user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Frontend Implementation
&lt;/h3&gt;

&lt;p&gt;On the frontend, we provide a dedicated Hook to manage image attachment state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useImageAttachmentManager&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/hooks/useImageAttachmentManager&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ChatInput&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;uploadedImages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;hasBlockingAttachments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;isUploading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;selectFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;removeAttachment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;clearAttachments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useImageAttachmentManager&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;ownerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mapUploadedImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;uploadOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleFileSelect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;selectFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handlePaste&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ClipboardEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboardData&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;handleFileSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Attachment bar */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;att&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AttachmentItem&lt;/span&gt;
          &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;att&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;att&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;att&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;onRemove&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;removeAttachment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;att&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localId&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
        &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;))}&lt;/span&gt;

      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Input box */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt; &lt;span class="nx"&gt;onPaste&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handlePaste&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Upload button */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;fileInputRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;Upload&lt;/span&gt; &lt;span class="nx"&gt;Image&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This Hook encapsulates all attachment management logic, including upload status tracking, failure retry, attachment deletion, etc. It's very simple to use—just calling a few methods completes the entire process. Good API design is like this—simple and easy to use, yet flexible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parsing Custom Protocol&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Extract sessionId and imageId from custom protocol&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseHagiImageUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hagiimag://session-abc123/20260301-143022-uuid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Returns: { sessionId: "session-abc123", imageId: "20260301-143022-uuid" }&lt;/span&gt;

&lt;span class="c1"&gt;// Build preview URL&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;previewUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildPreviewUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Returns: "/api/Images/session-abc123/20260301-143022-uuid/content"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Through these two utility functions, the frontend can easily convert between &lt;code&gt;hagiimag://&lt;/code&gt; protocol and HTTP URLs. This conversion logic is encapsulated, making it much more convenient to use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend Implementation
&lt;/h3&gt;

&lt;p&gt;The backend uses ASP.NET Core implementation, with &lt;code&gt;ImagesController&lt;/code&gt; and &lt;code&gt;ImagesDomainService&lt;/code&gt; at the core:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"upload"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;RequestSizeLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ImageUploadResponseDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromForm&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;UploadImageFormRequest&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Validate request&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UserFriendlyException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No file provided"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Validate file type and size&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errorMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_imagesDomainService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UserFriendlyException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errorMessage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Save to file system&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenReadStream&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_imagesDomainService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UploadImageAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CurrentUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Compress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Return result&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This implementation follows typical Web API development patterns: validate, process, return. Note that we set a 50MB request size limit to prevent malicious large file uploads. In the online world, it's always better to be cautious.&lt;/p&gt;

&lt;h3&gt;
  
  
  Important Considerations
&lt;/h3&gt;

&lt;p&gt;During implementation, some details need special attention:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permission validation&lt;/strong&gt;: Image access must verify user identity, ensuring only images from their own sessions can be accessed. This is a basic security requirement that cannot be omitted. When it comes to security, better safe than sorry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path security&lt;/strong&gt;: Strictly validate &lt;code&gt;sessionId&lt;/code&gt; and &lt;code&gt;imageId&lt;/code&gt; to prevent path traversal attacks. For example, reject paths containing &lt;code&gt;../&lt;/code&gt; to prevent users from accessing arbitrary files in the system. Handling these boundary conditions well makes the system more robust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File cleanup&lt;/strong&gt;: When sessions are deleted, associated images must be cleaned up synchronously to avoid orphan file accumulation. Over long operation periods, these files may occupy significant disk space. Timely cleanup is also a good habit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compression strategy&lt;/strong&gt;: For screenshot-type filenames (like &lt;code&gt;screenshot.png&lt;/code&gt;), automatically enable compression to save space. This strategy can be adjusted according to actual needs. When it comes to storage space, every bit saved helps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fallback handling&lt;/strong&gt;: Executors that don't support multimodal must receive image path hints and cannot silently drop image information. This is important, otherwise users will think the AI ignored their image. User experience depends on these details.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State management&lt;/strong&gt;: Attachments being uploaded block message sending, failed attachments allow retry or deletion. This design ensures user experience continuity. Clear state management means users won't feel confused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Through this complete image upload and recognition solution, HagiCode achieved a full loop from user input to AI recognition. The core highlights of the entire solution include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom &lt;code&gt;hagiimag://&lt;/code&gt; protocol achieves standardization of image references&lt;/li&gt;
&lt;li&gt;File system storage simplifies implementation and improves performance&lt;/li&gt;
&lt;li&gt;Frontend preview and AI access separation balances security and usability&lt;/li&gt;
&lt;li&gt;Immediate upload strategy optimizes user experience&lt;/li&gt;
&lt;li&gt;Multimodal and text fallback compatibility design ensures flexibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This solution runs stably in HagiCode with positive user feedback. If you're also implementing similar functionality, I hope these experiences are helpful to you.&lt;/p&gt;

&lt;p&gt;Actually, when it comes to technical solutions, there's no absolute right or wrong, only what fits or doesn't fit. Finding the path that suits your project is what's most important.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;HagiCode GitHub: &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;github.com/HagiCode-org/site&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;HagiCode Official Site: &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;hagicode.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenSpec Workflow Documentation: &lt;a href="https://docs.hagicode.com" rel="noopener noreferrer"&gt;docs.hagicode.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Original Article &amp;amp; License
&lt;/h2&gt;

&lt;p&gt;Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.&lt;br&gt;
This article was created with AI assistance and reviewed by the author before publication.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author: &lt;a href="https://www.newbe.pro" rel="noopener noreferrer"&gt;newbe36524&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Original URL: &lt;a href="https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-05-07-chat-image-upload-ai-recognition%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-05-07-chat-image-upload-ai-recognition%2F&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>hagicode</category>
    </item>
    <item>
      <title>Customizing OpenSpec Steps to Improve AI Generation Results</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Thu, 07 May 2026 02:25:58 +0000</pubDate>
      <link>https://dev.to/newbe36524/customizing-openspec-steps-to-improve-ai-generation-results-1b0b</link>
      <guid>https://dev.to/newbe36524/customizing-openspec-steps-to-improve-ai-generation-results-1b0b</guid>
      <description>&lt;h1&gt;
  
  
  Customizing OpenSpec Steps to Improve AI Generation Results
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;When using OpenSpec to manage technical proposals, we encountered inconsistent quality in AI-generated documentation. There was really no other way but to modify the prompt templates ourselves. This article documents those days.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;OpenSpec is a system for managing technical proposals with a simple core idea: input a change description, automatically generate various documentation artifacts. Proposals, designs, specs, tasks—all can be auto-generated. Sounds pretty ideal, right?&lt;/p&gt;

&lt;p&gt;But in actual use, we discovered some issues. How should I put it—not major problems, just that the generated output didn't feel quite right.&lt;/p&gt;

&lt;p&gt;The generated &lt;code&gt;design.md&lt;/code&gt; lacked necessary visual elements—no Mermaid flowcharts, no sequence diagrams, and no architecture diagrams. Such design documents made the technical team shake their heads; after all, who wants to read walls of pure text?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;proposal.md&lt;/code&gt; was also unsatisfactory, lacking code change tables and UI prototypes. Decision-makers could stare at it for ages and still not understand what the change actually modified.&lt;/p&gt;

&lt;p&gt;More frustrating was &lt;code&gt;tasks.md&lt;/code&gt;, which mixed in various Git operation tasks. Responsibility boundaries became unclear, and developers looking at these tasks didn't know what they should or shouldn't do. This is also a bit helpless—after all, AI doesn't know your team's division of labor.&lt;/p&gt;

&lt;p&gt;Visualization requirements for different document levels were also unclear. What charts should proposal and design contain? This question constantly troubled the team.&lt;/p&gt;

&lt;p&gt;Where's the root of these problems? After analysis, we discovered the key point: the prompt templates lacked clear constraints and guidance.&lt;/p&gt;

&lt;p&gt;This isn't surprising—after all, templates themselves are generic and can't perfectly adapt to every team's needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution shared in this article comes from our practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode is an AI code assistant project, and we heavily use OpenSpec to manage technical proposals during development.&lt;/p&gt;

&lt;p&gt;It was precisely these real-world experiences that led to the birth of this improvement plan. Actually, it's nothing special—just encountering problems and solving them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analysis: Prompt System Architecture
&lt;/h2&gt;

&lt;p&gt;To solve problems, first understand the system. Let's see how OpenSpec's prompt system works.&lt;/p&gt;

&lt;p&gt;OpenSpec uses the Handlebars template system, where each prompt contains two parts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSON metadata file&lt;/strong&gt;: Defines parameters, scenarios, version information&lt;br&gt;
&lt;strong&gt;Handlebars template file&lt;/strong&gt;: Contains actual prompt content&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Resources/Prompts/
├── openspec-v1-ff.zh-CN.json    # metadata
├── openspec-v1-ff.zh-CN.hbs     # template content
├── openspec-v1-ff.en-US.json
└── openspec-v1-ff.en-US.hbs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The advantages of this separation design are obvious: metadata and content are managed separately, facilitating maintenance and localization. It's also a bit like writing code—separation of logic and presentation, everyone understands this principle.&lt;/p&gt;

&lt;p&gt;The FF (Fast Forward) workflow is OpenSpec's core generation process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[User inputs change description] --&amp;gt; B[Create change directory]
    B --&amp;gt; C[Get artifact build order]
    C --&amp;gt; D[Create artifacts in dependency order]
    D --&amp;gt; E[Check planning direction requirements]
    E --&amp;gt; F[Verify artifact completeness]
    F --&amp;gt; G[Display final state]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This process looks perfect, but the problem lies in the "planning direction requirements" step—it lacks sufficiently clear guidance.&lt;/p&gt;

&lt;p&gt;This is also a bit helpless; after all, when designing the system, it's impossible to consider every team's specific needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Planning Direction System
&lt;/h2&gt;

&lt;p&gt;The planning direction system is OpenSpec's core customization mechanism, allowing users to select different generation options. The HagiCode project defines the following directions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Direction ID&lt;/th&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Default Enabled&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;explore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exploration mode&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;change-map&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Change map&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flowchart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Interactive flowchart&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;prototype&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;UI prototype&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;architecture&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Architecture diagram&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sequence&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;API sequence diagram&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each direction defines stable identifiers, default enabled states, display labels, and Chinese/English prompt fragments.&lt;/p&gt;

&lt;p&gt;This system design is clever, but in HagiCode's practice, we discovered that having definitions alone isn't enough—the planning directions need to be explicitly used in prompt templates.&lt;/p&gt;

&lt;p&gt;This is also a bit like many things in life: having options doesn't mean making choices; someone still needs to tell you how to choose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution: Clear Constraints and Examples
&lt;/h2&gt;

&lt;p&gt;Our improvement approach is straightforward: add clear constraints and reference examples to prompt templates.&lt;/p&gt;

&lt;p&gt;Actually, there's nothing special—just making things clear.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Add Document Visualization Requirements
&lt;/h3&gt;

&lt;p&gt;In the &lt;code&gt;openspec-v1-ff.zh-CN.hbs&lt;/code&gt; template, we added explicit content scope constraints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### tasks.md Content Scope Constraints&lt;/span&gt;

When creating &lt;span class="sb"&gt;`tasks.md`&lt;/span&gt; artifacts, the following content scope constraints must be observed:

Must include:
&lt;span class="p"&gt;-&lt;/span&gt; Business logic tasks (code implementation, feature development)
&lt;span class="p"&gt;-&lt;/span&gt; Technical implementation tasks (component integration, API development)
&lt;span class="p"&gt;-&lt;/span&gt; Testing tasks (unit tests, integration tests)
&lt;span class="p"&gt;-&lt;/span&gt; Documentation tasks (updating documentation, adding comments)

Must not include:
&lt;span class="p"&gt;-&lt;/span&gt; Git commit operations (git add, git commit, git push)
&lt;span class="p"&gt;-&lt;/span&gt; Version control management workflows
&lt;span class="p"&gt;-&lt;/span&gt; Deployment and release operations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using standardized "MUST/MUST NOT" language rather than "suggested" or "may" allows AI to more accurately understand constraints.&lt;/p&gt;

&lt;p&gt;This is also a bit like teaching children—say what you mean, no ambiguity allowed.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Provide Reference Examples for Each Direction
&lt;/h3&gt;

&lt;p&gt;Just saying "include flowcharts" isn't enough. We provided specific output examples for each enabled direction.&lt;/p&gt;

&lt;p&gt;After all, talk is cheap—give a concrete example, and AI can better understand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Change map direction example&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| File path | Change type | Change reason | Impact scope |
|-----------|-------------|---------------|-------------|
| Path/to/file | Add | Description | Module name |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Prototype direction example&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────┐
│ User Login                            [×] │
├─────────────────────────────────────────┤
│  Email address *                       │
│ ┌─────────────────────────────────────┐ │
│ │ user@example.com                   │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Flowchart direction example&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    participant U as User
    participant UI as Login Interface
    participant API as Backend API
    U-&amp;gt;&amp;gt;UI: Click login button
    UI-&amp;gt;&amp;gt;API: POST /api/auth/login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These examples allow AI to accurately understand the expected output format rather than improvising.&lt;/p&gt;

&lt;p&gt;This is also a bit like providing reference answers during an exam—while not exactly the same, the format should at least be correct.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Use Standardized Language for Clear Requirements
&lt;/h3&gt;

&lt;p&gt;For visualization requirements of different document types, we use standardized language to constrain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;For proposal.md:
&lt;span class="p"&gt;-&lt;/span&gt; Must include code change table (when change-map direction enabled)
&lt;span class="p"&gt;-&lt;/span&gt; Must include UI prototype (when involving UI changes and prototype direction enabled)
&lt;span class="p"&gt;-&lt;/span&gt; Must not include detailed architecture diagrams (these should be in design.md)

For design.md:
&lt;span class="p"&gt;-&lt;/span&gt; Must include all proposal.md content (more detailed version)
&lt;span class="p"&gt;-&lt;/span&gt; Must include architecture diagram (when architecture direction enabled)
&lt;span class="p"&gt;-&lt;/span&gt; Must include data flow diagram (when flowchart direction enabled)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These clear constraints significantly improved generation quality.&lt;/p&gt;

&lt;p&gt;Actually, there's nothing else—just making things clear, don't let AI guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice: Code Implementation
&lt;/h2&gt;

&lt;p&gt;Theory covered, let's see how it's implemented in the HagiCode project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Define Planning Directions
&lt;/h3&gt;

&lt;p&gt;Define planning directions in &lt;code&gt;ProposalPlanningDirections.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProposalPlanningDirections&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ProposalPlanningDirectionDefinition&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;Catalog&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;ChangeMapId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"Change map"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;DefaultEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;EnglishPromptFragment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="s"&gt;"- Change map: include structured file-impact views..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ChinesePromptFragment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="s"&gt;"- 变更地图：加入结构化的文件影响视图..."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="c1"&gt;// ... other directions&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;RenderInstructionBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProposalPlanningDirectionState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;directions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;enabledDirections&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;directions&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Enabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enabledDirections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;heading&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;IsChineseLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"本次生成启用以下规划方向："&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Apply the following planning directions:"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewLine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;..&lt;/span&gt; &lt;span class="n"&gt;enabledDirections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetPromptFragment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;))]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code has several noteworthy design points:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Using arrays instead of lists because definitions don't change at runtime&lt;/li&gt;
&lt;li&gt;Lazy rendering—only generate text when there are enabled directions&lt;/li&gt;
&lt;li&gt;Multi-language support, selecting appropriate prompt fragments based on locale&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Actually, there's nothing special—just some常规 code design.&lt;/p&gt;

&lt;h3&gt;
  
  
  Template Parameterization
&lt;/h3&gt;

&lt;p&gt;Use conditional statements in Handlebars templates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="k"&gt;{{#if&lt;/span&gt; &lt;span class="nv"&gt;planningDirectionInstructions&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
## Planning Directions for This Generation

&lt;span class="k"&gt;{{{&lt;/span&gt;&lt;span class="nv"&gt;planningDirectionInstructions&lt;/span&gt;&lt;span class="k"&gt;}}}&lt;/span&gt;
&lt;span class="k"&gt;{{/if}}&lt;/span&gt;

**Steps**
1. **If input not provided, use reasonable defaults**
2. **Create change directory**
3. **Get artifact build order**
4. **Create artifacts sequentially until apply-ready**
   a. For each ready artifact:
      - Get instructions
      - Read dependency files
      - Create artifact file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;{{{planningDirectionInstructions}}}&lt;/code&gt;—three curly braces mean don't escape HTML, which preserves formats like Mermaid code blocks.&lt;/p&gt;

&lt;p&gt;This is also a bit like compromise in life—sometimes you need to keep some original content, can't escape everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt Loading Implementation
&lt;/h3&gt;

&lt;p&gt;Implement parameterized prompt loading through &lt;code&gt;FilePromptProvider&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetOpenspecV1FfPromptAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;changeName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;changeDescription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"en-US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;planningDirectionInstructions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"planningDirectionInstructions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nf"&gt;ResolvePlanningDirectionInstructions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;planningDirectionInstructions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;changeName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"changeName"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;changeName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;GetPromptWithParametersAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;PromptScenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OpenspecV1Ff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design is flexible: &lt;code&gt;planningDirectionInstructions&lt;/code&gt; is optional—if not provided, the system uses default configuration.&lt;/p&gt;

&lt;p&gt;After all, no one wants to pass in a bunch of parameters every time; having a default value is always good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation and Testing
&lt;/h2&gt;

&lt;p&gt;After implementation, the HagiCode team conducted comprehensive validation:&lt;/p&gt;

&lt;h3&gt;
  
  
  When Specific Directions Are Enabled
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Check if generated proposal.md contains code change table&lt;/li&gt;
&lt;li&gt;Check if generated design.md contains architecture diagrams&lt;/li&gt;
&lt;li&gt;Verify tasks.md doesn't include Git operation tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When Specific Directions Are Disabled
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Verify corresponding visualization content isn't generated&lt;/li&gt;
&lt;li&gt;Ensure other directions' output isn't affected&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Edge Cases
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Behavior when all directions are disabled&lt;/li&gt;
&lt;li&gt;Error handling for invalid direction IDs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tests ensure system stability and predictability—critical for team adoption of new tools.&lt;/p&gt;

&lt;p&gt;Actually, there's nothing special—just test what should be tested, after all, no one wants problems after going live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Considerations
&lt;/h2&gt;

&lt;p&gt;When implementing this solution, avoid these pitfalls:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Template synchronization&lt;/strong&gt;: When modifying templates, keep them in sync with upstream. The HagiCode team encountered a template conflict that took half a day to resolve. This is also a bit helpless—after all, upgrades always bring some compatibility issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bilingual consistency&lt;/strong&gt;: Ensure Chinese and English templates have consistent structure and constraints. We encountered a situation where the Chinese version had constraints but the English version didn't, causing inconsistent document quality. This is also a bit awkward—after all, who knows which language users will use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance impact&lt;/strong&gt;: Planning direction rendering should complete in microseconds. If rendering takes too long, it affects user experience. After all, who wants to wait ages to see results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backward compatibility&lt;/strong&gt;: Maintain support for old version APIs. For example, the &lt;code&gt;enableExploreMode&lt;/code&gt; parameter—although we now use the planning direction system, old code still uses it. This is also a bit helpless—can't always require everyone to upgrade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clear expression&lt;/strong&gt;: Use standardized language (MUST/SHALL) rather than suggestive language. This point was fully validated in HagiCode's practice. Actually, there's nothing else—just making things clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;By customizing OpenSpec prompt steps, we successfully improved the quality of AI-generated documentation. Key improvements include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Adding clear constraint conditions to prompt templates&lt;/li&gt;
&lt;li&gt;Providing specific output examples for each planning direction&lt;/li&gt;
&lt;li&gt;Using standardized language (MUST/MUST NOT) to constrain AI behavior&lt;/li&gt;
&lt;li&gt;Implementing flexible parameterized prompt loading through code&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This solution was validated in the HagiCode project, with significantly improved document quality: design documents include complete visual elements, proposal documents have clear code change tables, and task lists have clear responsibilities.&lt;/p&gt;

&lt;p&gt;Actually, it's nothing special—just solving the problem.&lt;/p&gt;

&lt;p&gt;If you're also using similar AI-assisted documentation generation systems, I hope these experiences help you. Remember: clear constraints and concrete examples are key to obtaining high-quality output.&lt;/p&gt;

&lt;p&gt;After all, for some things, it's better to be clear...&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode Project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.hagicode.com" rel="noopener noreferrer"&gt;OpenSpec Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://handlebarsjs.com/" rel="noopener noreferrer"&gt;Handlebars Template Syntax&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mermaid.js.org/" rel="noopener noreferrer"&gt;Mermaid Diagram Syntax&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>openspec</category>
      <category>ai</category>
      <category>hagicode</category>
    </item>
    <item>
      <title>How to Integrate GPT, Claude, and Other AI Models Using Copilot CLI</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Wed, 06 May 2026 11:11:59 +0000</pubDate>
      <link>https://dev.to/newbe36524/how-to-integrate-gpt-claude-and-other-ai-models-using-copilot-cli-5e86</link>
      <guid>https://dev.to/newbe36524/how-to-integrate-gpt-claude-and-other-ai-models-using-copilot-cli-5e86</guid>
      <description>&lt;h1&gt;
  
  
  How to Integrate GPT, Claude, and Other AI Models Using Copilot CLI
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;In AI application development, how can you use a unified interface to integrate multiple models like GPT and Claude? This article shares our AI provider system design based on Orleans Grain architecture and practical GitHub Copilot CLI integration experience.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;In modern AI application development, integrating the latest GPT models is a core requirement for many developers. GitHub Copilot CLI is a powerful tool that not only supports OpenAI's GPT series models (such as GPT-4, GPT-5), but also other mainstream AI models like Claude. Through Copilot CLI, developers can call different AI models using a unified command-line interface without implementing complex integration logic for each model separately.&lt;/p&gt;

&lt;p&gt;Actually, this is a long-standing issue. Having to write call logic for each model is just painful—too much heartache. After all, no one enjoys writing repetitive code, and rather than reinventing the wheel, it's better to find a unified interface to handle everything. Copilot CLI is exactly that kind of existence—you just call it, and let it handle the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core Values&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unified CLI interface for accessing multiple AI models&lt;/li&gt;
&lt;li&gt;Support for session management and context retention&lt;/li&gt;
&lt;li&gt;Built-in tool calling capabilities (file operations, Git operations, etc.)&lt;/li&gt;
&lt;li&gt;Support for streaming responses and real-time output&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution shared in this article comes from our practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode is an AI code assistant project. During development, we encountered the challenge of needing to support multiple AI models simultaneously—some users are accustomed to using GPT-4, some prefer Claude, and others want to try the latest GPT-5. If we implemented separate call logic for each model, the code would become difficult to maintain. Through Copilot CLI's unified interface, we successfully solved this multi-model support pain point.&lt;/p&gt;

&lt;p&gt;To put it plainly, users just have diverse tastes—difficult to please everyone. Some like GPT, some prefer Claude, and others insist on using the latest GPT-5. We just want everyone to be able to use their favorite model—after all, being happy is what matters most.&lt;/p&gt;

&lt;h2&gt;
  
  
  System Architecture Design
&lt;/h2&gt;

&lt;p&gt;We implemented an extensible AI provider system through Orleans Grain architecture with the following overall structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────┐
│  Frontend/Client │
└────────┬────────┘
         │
         ▼
┌─────────────────────────────────┐
│  IGitHubCopilotGrain (Interface)│
│  - ExecuteCommandStreamAsync    │
│  - RunEditAsync                 │
│  - CancelAsync                  │
└────────┬────────────────────────┘
         │
         ▼
┌─────────────────────────────────┐
│  GitHubCopilotGrain (Implementation)│
│  - State Management             │
│  - Session Binding              │
│  - Response Mapping             │
└────────┬────────────────────────┘
         │
         ▼
┌─────────────────────────────────┐
│  CopilotAIProvider (Provider)   │
│  - Configuration Parsing        │
│  - Permission Management        │
│  - Streaming Processing         │
└────────┬────────────────────────┘
         │
         ▼
┌─────────────────────────────────┐
│  HagiCode.Libs (Shared Runtime) │
│  - Copilot CLI Process Management│
│  - Message Protocol Parsing     │
│  - Session Retention            │
└─────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The advantage of this architecture is &lt;strong&gt;clear layering with single responsibilities&lt;/strong&gt;. The interface layer defines the unified AI service contract, the implementation layer handles Orleans' distributed state management, the provider layer encapsulates Copilot CLI interaction details, and the underlying runtime is responsible for communicating with the CLI process.&lt;/p&gt;

&lt;p&gt;To put it simply, clarify who does what—don't mix things up. After all, once code gets messy, it's hard to change later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Component Analysis
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. GitHubCopilotGrain: Distributed AI Service Interface
&lt;/h3&gt;

&lt;p&gt;As an Orleans Grain implementation, &lt;code&gt;GitHubCopilotGrain&lt;/code&gt; provides distributed AI service capabilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IGitHubCopilotGrain&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IGrainWithStringKey&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Execute command and stream response&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IAsyncEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GitHubCopilotResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ExecuteCommandStreamAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;heroId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;executionMessageId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;systemMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;requestSettings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Execute edit operation&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IAsyncEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GitHubCopilotResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;RunEditAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;editCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;heroId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Cancel current execution&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CancelAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;heroId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Design Points&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using &lt;code&gt;IAsyncEnumerable&lt;/code&gt; to support streaming responses, avoiding long wait times&lt;/li&gt;
&lt;li&gt;Session-level state isolation through &lt;code&gt;heroId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Support for passing &lt;code&gt;requestSettings&lt;/code&gt; to dynamically configure model parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. CopilotAIProvider: Core Provider Implementation
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CopilotAIProvider&lt;/code&gt; is the core of the entire solution, encapsulating all interaction logic with Copilot CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CopilotAIProvider&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAIProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IVersionedAIProvider&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;CopilotOptions&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ICopilotProcessExecutor&lt;/span&gt; &lt;span class="n"&gt;_executor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;IAsyncEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AIStreamingChunk&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SendMessageAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;AIRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;embeddedCommandPrompt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnumeratorCancellation&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Build execution options&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CopilotOptions&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;SessionId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;GetValueOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"copilotSessionId"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;Timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;PermissionMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OperationType&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;AIOperationType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Edit&lt;/span&gt;
                &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;CopilotPermissionMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BypassPermissions&lt;/span&gt;
                &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CopilotPermissionMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="c1"&gt;// Execute command and stream process response&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;BuildChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Core Features&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automatic retry mechanism&lt;/strong&gt;: Handles transient network issues and CLI process exceptions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning content tracking&lt;/strong&gt;: Captures the model's reasoning process (reasoning field)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple message type handling&lt;/strong&gt;: Supports assistant, tool.started, tool.completed and other messages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission mode switching&lt;/strong&gt;: Edit operations automatically use bypassPermissions, regular queries use default&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. CopilotOptions: Flexible Configuration System
&lt;/h3&gt;

&lt;p&gt;The configuration class supports rich option settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CopilotOptions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Specify the model to use, such as "gpt-4", "gpt-5", "claude-opus-4.5"&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gpt-4"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Copilot CLI executable path&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ExecutablePath&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"copilot"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Session timeout&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;Timeout&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Authentication method&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;CopilotAuthSource&lt;/span&gt; &lt;span class="n"&gt;AuthSource&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CopilotAuthSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoggedInUser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Permission mode&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;CopilotPermissionMode&lt;/span&gt; &lt;span class="n"&gt;PermissionMode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CopilotPermissionMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Session ID for maintaining context&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;SessionId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;/// Tool permission configuration&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;CopilotToolPermissions&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Permissions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configuration is about being adequate—after all, who wants to write a bunch of configurations they'll never use? Covering most scenarios is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration Guide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Basic Configuration
&lt;/h3&gt;

&lt;p&gt;Add Copilot provider configuration in &lt;code&gt;appsettings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"AI"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Providers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Providers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"GitHubCopilot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ExecutablePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"copilot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"IdleTimeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"UseLoggedInUser"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"NoAskUser"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"PermissionMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"AllowAllTools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"AllowAllPaths"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"AllowedTools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(cat:*)"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"DeniedTools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Model Selection
&lt;/h3&gt;

&lt;p&gt;The system supports the following models (specified via Copilot CLI's &lt;code&gt;--model&lt;/code&gt; parameter):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Recommended Scenarios&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gpt-4 / gpt-4-turbo&lt;/td&gt;
&lt;td&gt;OpenAI 4th generation models&lt;/td&gt;
&lt;td&gt;General tasks, cost-effective&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-5&lt;/td&gt;
&lt;td&gt;OpenAI latest 5th generation model&lt;/td&gt;
&lt;td&gt;Complex reasoning, best performance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;claude-sonnet-4.5&lt;/td&gt;
&lt;td&gt;Anthropic Sonnet 4.5&lt;/td&gt;
&lt;td&gt;Balance performance and cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;claude-opus-4.5&lt;/td&gt;
&lt;td&gt;Anthropic Opus 4.5&lt;/td&gt;
&lt;td&gt;High-precision tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In HagiCode's practice, we use GPT-4 as the default daily model, switch to GPT-5 for complex tasks (like large refactoring), and offer Claude models as an alternative for users who prefer Anthropic.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Register Services
&lt;/h3&gt;

&lt;p&gt;Register related services in the DI container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Register Copilot AI provider&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IAIProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CopilotAIProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Register Orleans Grain&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IGitHubCopilotGrain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GitHubCopilotGrain&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Register process executor&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ICopilotProcessExecutor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CopilotProcessExecutor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Actually just these few lines—nothing special. Just register what needs to be registered so it can be found when needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Basic Call
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Get Grain&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;grain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;grainFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetGrain&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IGitHubCopilotGrain&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"session-123"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Execute command&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;grain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteCommandStreamAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"Analyze the code structure of the current directory and generate documentation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;heroId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;ExecutorResponseType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;ExecutorResponseType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToolCall&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"[Tool Call] &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToolName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;ExecutorResponseType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Completion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"\n[Complete] Token Usage: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PromptTokens&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletionTokens&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Context-Aware Session
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;requestSettings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gpt-5"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"0.7"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"maxTokens"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"4096"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"copilotSessionId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"existing-session-123"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// Maintain session context&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;grain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteCommandStreamAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"Based on the previous analysis, generate corresponding unit tests"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;requestSettings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;requestSettings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Handle response&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Edit Mode Call
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;grain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunEditAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"Convert all PascalCase naming to camelCase"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;heroId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"hero-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ExecutorResponseType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileEdit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"[Edit] &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FilePath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EditCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; changes"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Session Retention
&lt;/h3&gt;

&lt;p&gt;Using the &lt;code&gt;copilotSessionId&lt;/code&gt; parameter allows maintaining context across requests, which is very useful for scenarios requiring multi-turn dialogue. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Round 1: Establish context&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;settings1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"copilotSessionId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"session-001"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;grain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteCommandStreamAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"This is a C# project using .NET 8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requestSettings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;settings1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Round 2: Ask based on context&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;settings2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"copilotSessionId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"session-001"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;grain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteCommandStreamAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Recommend suitable project structure"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requestSettings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;settings2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After all, AI isn't omnipotent—without context, how does it know what you're talking about? Like chatting, you need back-and-forth to keep the conversation going.&lt;/p&gt;

&lt;h3&gt;
  
  
  Permission Control
&lt;/h3&gt;

&lt;p&gt;Choose the appropriate permission mode based on operation type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query operations&lt;/strong&gt;: Use &lt;code&gt;default&lt;/code&gt; mode, allowing AI to only read files and execute safe Git commands&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edit operations&lt;/strong&gt;: Use &lt;code&gt;bypassPermissions&lt;/code&gt; mode, allowing AI to modify files
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;permissionMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;operationType&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;AIOperationType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Edit&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;CopilotPermissionMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BypassPermissions&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CopilotPermissionMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tool Whitelist
&lt;/h3&gt;

&lt;p&gt;Control AI executable operations through &lt;code&gt;AllowedTools&lt;/code&gt; configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AllowAllTools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AllowedTools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(cat:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Glob"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In HagiCode, we strictly limit AI's operation permissions, only allowing file reading and Git command execution to ensure system security.&lt;/p&gt;

&lt;p&gt;After all, you can't be too careful with security. Who knows if the AI might suddenly delete your entire project?&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeout Handling
&lt;/h3&gt;

&lt;p&gt;The default timeout is set to 30 minutes. For operations involving large numbers of files (like full code analysis), adjustments may be needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CopilotOptions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// Extend to 60 minutes&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Q: How to switch between different AI models?
&lt;/h3&gt;

&lt;p&gt;A: Specify through &lt;code&gt;Model&lt;/code&gt; configuration or &lt;code&gt;requestSettings&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"claude-opus-4.5"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Actually just changing a parameter—nothing complex.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q: How long can session context be maintained?
&lt;/h3&gt;

&lt;p&gt;A: Depends on Copilot CLI implementation, usually cleaned up after session idle timeout (default 5 minutes). Can be adjusted through &lt;code&gt;IdleTimeout&lt;/code&gt; configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q: How to handle CLI process crashes?
&lt;/h3&gt;

&lt;p&gt;A: &lt;code&gt;CopilotAIProvider&lt;/code&gt; has a built-in automatic retry mechanism that captures process exceptions and restarts the CLI. If consecutive failures exceed a threshold, it will throw an &lt;code&gt;AIProviderException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Program crashes are unavoidable. You can only do your best with fault tolerance—if it really goes down, just restart it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q: Are custom tools supported?
&lt;/h3&gt;

&lt;p&gt;A: The tools supported by Copilot CLI are predefined, but you can control which tools are available through &lt;code&gt;AllowedTools&lt;/code&gt; configuration. Custom tools require waiting for future Copilot CLI updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;By integrating multiple AI models through Copilot CLI, we solved the multi-model support challenge in HagiCode development. The core advantages of this solution are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unified Interface&lt;/strong&gt;: One codebase supporting multiple models like GPT, Claude, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session Management&lt;/strong&gt;: Automatic context retention and session isolation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool Integration&lt;/strong&gt;: Built-in common tools like file operations, Git operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming Response&lt;/strong&gt;: Real-time AI output returns, improved user experience&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security and Control&lt;/strong&gt;: Fine-grained permission control and tool whitelisting&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your project also needs to support multiple AI models, or you're looking for a mature CLI tool integration solution, why not try Copilot CLI? This architecture has been fully validated in HagiCode and can handle complex production environment requirements.&lt;/p&gt;

&lt;p&gt;After all, who wants to write separate call code for each model? Having a unified solution saves everyone trouble.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/copilot" rel="noopener noreferrer"&gt;GitHub Copilot CLI Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.microsoft.com/en-us/research/project/orleans/" rel="noopener noreferrer"&gt;Orleans Distributed Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode Project Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode Official Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.hagicode.com/installation/docker-compose" rel="noopener noreferrer"&gt;HagiCode Installation Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hagicode.com/desktop/" rel="noopener noreferrer"&gt;HagiCode Desktop Quick Install&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this article helps you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give us a Star on GitHub: &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;github.com/HagiCode-org/site&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Visit the official website to learn more: &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;hagicode.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Watch the official version demo video: &lt;a href="https://www.bilibili.com/video/BV1z4oWB3EpY/" rel="noopener noreferrer"&gt;www.bilibili.com/video/BV1z4oWB3EpY/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;One-click install to experience: &lt;a href="https://docs.hagicode.com/installation/docker-compose" rel="noopener noreferrer"&gt;docs.hagicode.com/installation/docker-compose&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Desktop quick install: &lt;a href="https://hagicode.com/desktop/" rel="noopener noreferrer"&gt;hagicode.com/desktop/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Beta testing has started, welcome to install and experience&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>gpt</category>
      <category>claude</category>
      <category>copilotcli</category>
    </item>
    <item>
      <title>Building Multi-Platform code-server and OmniRoute with GitHub Actions</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Wed, 06 May 2026 06:04:29 +0000</pubDate>
      <link>https://dev.to/newbe36524/building-multi-platform-code-server-and-omniroute-with-github-actions-4p7d</link>
      <guid>https://dev.to/newbe36524/building-multi-platform-code-server-and-omniroute-with-github-actions-4p7d</guid>
      <description>&lt;h1&gt;
  
  
  Building Multi-Platform code-server and OmniRoute with GitHub Actions
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Facing the need to build and publish across Linux, macOS, and Windows platforms with unified releases, we designed a GitHub Actions-based multi-platform CI/CD pipeline. It's not that difficult when you think about it, but the roadblocks can definitely make you pull your hair out. This article shares the design philosophy and implementation details of this pipeline—including, of course, the pits we stepped in along the way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/coder/code-server" rel="noopener noreferrer"&gt;code-server&lt;/a&gt; is an open-source project that runs VS Code in a browser, allowing developers to work through a web IDE on a remote server. As HagiCode Desktop integrates code-server as its built-in runtime, we need to build, verify, and distribute customized versions of code-server across different operating systems (Linux, macOS, Windows).&lt;/p&gt;

&lt;p&gt;This should have been pretty straightforward, but... when is life ever that easy?&lt;/p&gt;

&lt;p&gt;Meanwhile, &lt;a href="https://github.com/diegosouzapw/OmniRoute" rel="noopener noreferrer"&gt;OmniRoute&lt;/a&gt; as a multi-model routing service also needs to share the same build and release pipeline with code-server. Although the two packages are built differently, they ultimately need to converge into the same GitHub Release. Like two originally non-intersecting lines, they still meet at some point in the end—call it fate, I suppose.&lt;/p&gt;

&lt;p&gt;This brings several engineering challenges:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cross-platform build differences&lt;/strong&gt;: The build toolchains for Linux, macOS, and Windows are completely different (Linux uses quilt + bash, macOS uses Homebrew, Windows requires MSYS2)—each platform has its own temperament&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build artifact verification&lt;/strong&gt;: After building, artifacts need to be automatically verified to start properly—after all, nobody wants to release something that doesn't run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unified version management&lt;/strong&gt;: Two packages need to share the same version number and release tag—like two people sharing one name, there needs to be a system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallel builds with serial publishing&lt;/strong&gt;: Builds can run in parallel, but publishing needs coordination—this is where mistakes happen, and when they do, they're really mistakes&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution shared in this article comes from practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode is an AI code assistant project that integrates code-server as a built-in runtime in its desktop product, thus requiring engineering solutions for multi-platform building and publishing. This is, to put it bluntly, just about getting the product out—nothing more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations of Upstream Build Pipeline
&lt;/h3&gt;

&lt;p&gt;The code-server upstream project's own CI/CD pipeline (&lt;code&gt;build.yaml&lt;/code&gt;) only builds for the &lt;code&gt;linux-x64&lt;/code&gt; platform, and its release process (&lt;code&gt;publish.yaml&lt;/code&gt;) only targets npm, AUR, and Docker channels. It doesn't support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native builds for macOS and Windows—perhaps they don't think these platforms are important enough&lt;/li&gt;
&lt;li&gt;Multi-platform matrix parallel builds—maybe the upstream team is small&lt;/li&gt;
&lt;li&gt;Unified artifact verification mechanism—just publish it and let users try it themselves&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's fine, every project has its own priorities. We just happen to need these features, so we'll build them ourselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design Decisions
&lt;/h3&gt;

&lt;p&gt;Based on the above analysis, HagiCode designed an independent build pipeline in &lt;code&gt;repos/vendered&lt;/code&gt; with the following core decisions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Reuse shared version management and release toolchain&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Version numbers use UTC date format &lt;code&gt;YYYY.MMDD.RRRR&lt;/code&gt;, where &lt;code&gt;RRRR&lt;/code&gt; is a zero-padded sequence of the GitHub Actions run number. This ensures monotonic incrementing and traceability of versions—after all, time doesn't flow backward, just like some things once changed cannot be undone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/versioning.mjs&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatDateVersion&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;revision&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;normalizedDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUTCFullYear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalizedDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUTCMonth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalizedDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUTCDate&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;month&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;normalizedRevision&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, the first build on 2026-05-05 generates version &lt;code&gt;2026.0505.0001&lt;/code&gt; and tag &lt;code&gt;v2026.0505.0001&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Actually, there's nothing special about this version format—it just happens to be good enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Package-isolated build scripts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each package (code-server, omniroute) maintains its own build and verification logic under &lt;code&gt;packages/&amp;lt;name&amp;gt;/scripts/&lt;/code&gt;, while shared publishing tools (&lt;code&gt;scripts/versioning.mjs&lt;/code&gt;, &lt;code&gt;scripts/github-release.mjs&lt;/code&gt;, &lt;code&gt;scripts/publication.mjs&lt;/code&gt;) remain package-agnostic. Each manages its own affairs without interfering—this is what's called "staying in one's lane."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Unified metadata contract&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All packages produce standardized &lt;code&gt;metadata.json&lt;/code&gt; containing &lt;code&gt;schemaVersion&lt;/code&gt;, &lt;code&gt;packageId&lt;/code&gt;, &lt;code&gt;version&lt;/code&gt;, &lt;code&gt;platform&lt;/code&gt;, &lt;code&gt;arch&lt;/code&gt;, &lt;code&gt;sourceRevision&lt;/code&gt;, and &lt;code&gt;artifacts[]&lt;/code&gt; fields, ensuring downstream consumers don't need to be aware of package differences. With a unified format, everyone can save some trouble.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Overall Workflow Architecture
&lt;/h3&gt;

&lt;p&gt;The entire pipeline is defined in &lt;code&gt;repos/vendered/.github/workflows/code-server-artifacts.yaml&lt;/code&gt; and includes the following stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prepare_release → build (matrix) → verify (matrix) → publish_github_release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The process is simple if you look at it simply, complex if you look at it complexly—it all depends on your perspective.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trigger Conditions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;          &lt;span class="c1"&gt;# Manual trigger&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;23&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;     &lt;span class="c1"&gt;# Daily scheduled build&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;          &lt;span class="c1"&gt;# Trigger on push to main branch&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                    &lt;span class="c1"&gt;# Only trigger on related file changes&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.github/workflows/code-server-artifacts.yaml"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.gitmodules"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scripts/**"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;packages/code-server/**"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;packages/omniroute/**"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The daily scheduled build is set for 3:23 AM—no particular reason, just picked a random time. Perhaps the person who chose this time didn't think too much about it either.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 1: Version Preparation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;prepare_release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-22.04&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.version.outputs.version }}&lt;/span&gt;
      &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.version.outputs.tag }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v6&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;version&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node ./scripts/versioning.mjs &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This stage generates a unified version number and Git tag, shared by all subsequent build and release steps. A good start saves a lot of trouble for the work ahead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2: Multi-Platform Matrix Build
&lt;/h3&gt;

&lt;p&gt;The build stage uses &lt;code&gt;strategy.matrix&lt;/code&gt; to execute in parallel across different platforms:&lt;/p&gt;

&lt;h4&gt;
  
  
  code-server Build Matrix
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;build_code_server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prepare_release&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;code-server Linux&lt;/span&gt;
          &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-22.04&lt;/span&gt;
          &lt;span class="na"&gt;artifact_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;code-server-linux&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;code-server macOS&lt;/span&gt;
          &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macos-latest&lt;/span&gt;
          &lt;span class="na"&gt;artifact_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;code-server-macos&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;code-server Windows&lt;/span&gt;
          &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;windows-latest&lt;/span&gt;
          &lt;span class="na"&gt;artifact_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;code-server-windows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key design: &lt;code&gt;fail-fast: false&lt;/code&gt; ensures that a failure on one platform doesn't cancel builds on other platforms. After all, one platform failing doesn't mean all platforms have issues—no need for everyone to go down together.&lt;/p&gt;

&lt;h4&gt;
  
  
  omniroute Build Matrix
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;build_omniroute&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prepare_release&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;omniroute Linux x64&lt;/span&gt;
          &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-22.04&lt;/span&gt;
          &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux&lt;/span&gt;
          &lt;span class="na"&gt;arch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;amd64&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;omniroute macOS x64&lt;/span&gt;
          &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macos-15-intel&lt;/span&gt;
          &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macos&lt;/span&gt;
          &lt;span class="na"&gt;arch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;amd64&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;omniroute macOS arm64&lt;/span&gt;
          &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macos-14&lt;/span&gt;
          &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macos&lt;/span&gt;
          &lt;span class="na"&gt;arch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arm64&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;omniroute Windows x64&lt;/span&gt;
          &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;windows-latest&lt;/span&gt;
          &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;windows&lt;/span&gt;
          &lt;span class="na"&gt;arch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;amd64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OmniRoute's matrix is richer, including both Intel and ARM architectures for macOS. Note that macOS ARM uses the &lt;code&gt;macos-14&lt;/code&gt; runner (Apple Silicon), while Intel uses &lt;code&gt;macos-15-intel&lt;/code&gt;. That's just how the world is—some things are always divided into camps—like Intel and ARM, never to reconcile.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 3: Platform-Specific Prerequisites
&lt;/h3&gt;

&lt;p&gt;Each platform requires different toolchains, and the workflow handles this through conditional steps:&lt;/p&gt;

&lt;h4&gt;
  
  
  Linux
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Linux prerequisites&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runner.os == 'Linux'&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sudo apt-get update &amp;amp;&amp;amp; sudo apt-get install -y jq rsync quilt libkrb5-dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  macOS
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install macOS prerequisites&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runner.os == 'macOS'&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;brew install jq rsync quilt python-setuptools&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Windows (MSYS2)
&lt;/h4&gt;

&lt;p&gt;Windows is the most complex, requiring MSYS2 to provide a Unix-like toolchain—there's no way around it, since Windows' design philosophy is completely different from Unix systems:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup MSYS2&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runner.os == 'Windows'&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;msys2/setup-msys2@v2&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;msystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MSYS&lt;/span&gt;
    &lt;span class="na"&gt;path-type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inherit&lt;/span&gt;
    &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
      &lt;span class="s"&gt;diffutils jq patch quilt rsync unzip zip&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure Windows shell paths&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runner.os == 'Windows'&lt;/span&gt;
  &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pwsh&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;Add-Content -Path $env:GITHUB_ENV -Value 'NPM_CONFIG_SCRIPT_SHELL=/usr/bin/bash'&lt;/span&gt;
    &lt;span class="s"&gt;Add-Content -Path $env:GITHUB_ENV -Value ("MSYS2_CMD={0}\\setup-msys2\\msys2.cmd" -f $env:RUNNER_TEMP)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Actually, these configurations aren't that complex, but the first time you encounter them, they can be pretty confusing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 4: Build Artifact Verification
&lt;/h3&gt;

&lt;p&gt;After building on each platform, verification steps download the artifacts, extract them, and actually start them to verify usability. After all, we don't want to release something that doesn't run—that would be too embarrassing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;verify_code_server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build_code_server&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;code-server Linux&lt;/span&gt;
          &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-22.04&lt;/span&gt;
          &lt;span class="na"&gt;bash_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;code-server Windows&lt;/span&gt;
          &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;windows-latest&lt;/span&gt;
          &lt;span class="na"&gt;bash_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;C:\msys64\usr\bin\bash.exe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verification script (&lt;code&gt;verify-startup.mjs&lt;/code&gt;) will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Extract the build artifacts&lt;/li&gt;
&lt;li&gt;Start code-server on a random available port&lt;/li&gt;
&lt;li&gt;Poll the &lt;code&gt;/healthz&lt;/code&gt; endpoint waiting for service readiness&lt;/li&gt;
&lt;li&gt;After confirming the service responds with 200, shut down the process
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForHealth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;requestHealth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Timed out waiting for code-server to become healthy`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Waiting for health checks always makes people a bit anxious—like waiting for someone who will never reply. Except this time the service will eventually start, while some people may never respond.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 5: Unified Publishing
&lt;/h3&gt;

&lt;p&gt;After all builds and verifications complete, the publishing stage collects the artifacts and creates a GitHub Release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;publish_github_release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;prepare_release&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build_code_server&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build_omniroute&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;verify_code_server&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;verify_omniroute&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
    &lt;span class="s"&gt;${{ (github.event_name == 'push' &amp;amp;&amp;amp; github.ref == 'refs/heads/main') ||&lt;/span&gt;
        &lt;span class="s"&gt;github.event_name == 'workflow_dispatch' }}&lt;/span&gt;
  &lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ format('vendered-github-release-{0}', needs.prepare_release.outputs.tag) }}&lt;/span&gt;
    &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency control&lt;/strong&gt;: Using &lt;code&gt;concurrency&lt;/code&gt; ensures that publishes for the same tag don't execute in parallel—avoiding duplicate releases is generally a good thing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditional publishing&lt;/strong&gt;: Only publish on push to &lt;code&gt;main&lt;/code&gt; branch or manual trigger; scheduled builds only execute build and verification&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artifact aggregation&lt;/strong&gt;: Use the &lt;code&gt;pattern&lt;/code&gt; parameter of &lt;code&gt;download-artifact&lt;/code&gt; to batch download all platform artifacts for both code-server and omniroute&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Practice
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Key Points for Cross-Platform Build Script Writing
&lt;/h3&gt;

&lt;p&gt;Build scripts (&lt;code&gt;build-artifacts.mjs&lt;/code&gt;) need to handle platform differences. Here are the key points:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Platform detection and normalization&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizePlatform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;darwin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;macos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;macos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;win32&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;windows&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;windows_nt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;windows&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;linux&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Different systems refer to the same platform differently—like the same person having different names in different contexts, but still being the same person.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Shell compatibility on Windows&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On Windows, &lt;code&gt;npm run&lt;/code&gt; calls &lt;code&gt;cmd.exe&lt;/code&gt;, but code-server's build scripts depend on bash. The solution is to set the &lt;code&gt;NPM_CONFIG_SCRIPT_SHELL&lt;/code&gt; environment variable and use MSYS2. There's no way around this, since Windows and Unix have completely different design philosophies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withCodeServerEnv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scriptShell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;windows&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/usr/bin/bash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BASH_PATH&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;NPM_CONFIG_SCRIPT_SHELL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;windows&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;scriptShell&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NPM_CONFIG_SCRIPT_SHELL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Artifact packaging&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Different platforms use different archive formats (Linux/macOS use &lt;code&gt;.tar.gz&lt;/code&gt;, Windows uses &lt;code&gt;.zip&lt;/code&gt;)—each platform has its own preferences, just like everyone has their own habits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;windows&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;powershell.exe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-NoLogo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-NoProfile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-Command&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`Compress-Archive -Path '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;releaseDir&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' -DestinationPath '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;archivePath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' -Force`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-czf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;archivePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-C&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;codeServerRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;releaseDir&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Patch management&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;code-server customization is implemented through quilt patches in the &lt;code&gt;patches/&lt;/code&gt; directory. Linux uses quilt directly, macOS installs quilt through Homebrew, and Windows needs to use quilt from MSYS2 or fall back to the &lt;code&gt;patch&lt;/code&gt; command (this part is quite troublesome):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Use patch command on Windows instead of quilt&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyPatchesWithPatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;series&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;codeServerRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;patches&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;series&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;patchFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;series&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\r?\n&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;patchFile&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;patchFiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runMsys2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`patch -p1 --forward -i "patches/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;patchFile&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;codeServerRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Windows part definitely took a lot of time—no way around it, since Windows' design philosophy is different from other systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version Number Design Considerations
&lt;/h3&gt;

&lt;p&gt;HagiCode uses the &lt;code&gt;YYYY.MMDD.RRRR&lt;/code&gt; format instead of upstream semantic versioning for the following reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Determinism&lt;/strong&gt;: Each build's version number is uniquely determined by date and run number&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monotonic incrementing&lt;/strong&gt;: Date prefix ensures natural sorting is chronological order&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source traceability&lt;/strong&gt;: Build time and CI run sequence number can be inferred from the version number&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Actually, there's nothing special about this—it just happens to be good enough. Semantic versioning sounds nice, but it's actually quite troublesome to use in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Important Notes
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Submodule recursive checkout&lt;/strong&gt;: Must use &lt;code&gt;submodules: recursive&lt;/code&gt; when building, ensuring complete cloning of upstream code for both code-server and omniroute (this place is easy to forget)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node version matching&lt;/strong&gt;: code-server build uses the Node version specified in upstream &lt;code&gt;.node-version&lt;/code&gt; file; omniroute uses Node 24&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows home directory&lt;/strong&gt;: OmniRoute on Windows CI needs to manually create &lt;code&gt;$HOME&lt;/code&gt; directory structure to avoid build scripts accessing non-existent paths—Windows directory structure is different from other systems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification timeout&lt;/strong&gt;: code-server startup verification has a 60-second timeout and needs adjustment based on actual startup speed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artifact slimming&lt;/strong&gt;: Delete embedded Node binary after building (&lt;code&gt;slimRelease&lt;/code&gt;), since downstream will use its own Node runtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish idempotency&lt;/strong&gt;: &lt;code&gt;github-release.mjs&lt;/code&gt; supports updating existing Releases (delete old Asset first then upload new one), ensuring retry safety&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are all lessons learned from stepping in pits—of course, when you're in the pit, it really makes you want to pull your hair out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Complete CI/CD Flow Diagram
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────┐
│                     Trigger Sources                              │
│  push to main / workflow_dispatch / cron(23 3 * * *)            │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────┐
│  prepare_release                                                 │
│  Generate version: 2026.0506.0001, tag: v2026.0506.0001         │
└──────────────────────────┬──────────────────────────────────────┘
                           │
              ┌────────────┼────────────┐
              ▼            ▼            ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ code-server  │ │ code-server  │ │ code-server  │
│ Linux        │ │ macOS        │ │ Windows      │
│ ubuntu-22.04 │ │ macos-latest │ │win-latest    │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
       │                │                │
       ▼                ▼                ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ verify       │ │ verify       │ │ verify       │
│ Linux        │ │ macOS        │ │ Windows      │
│ startup+healthz│ │ startup+healthz│ │ startup+healthz│
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
       │                │                │
       └────────────────┼────────────────┘
                        │
       ┌────────────────┼────────────────┐
       ▼                ▼                ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ omniroute    │ │ omniroute    │ │ omniroute    │ ...
│ linux-amd64  │ │ macos-amd64  │ │ macos-arm64  │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
       │                │                │
       └────────────────┼────────────────┘
                        │
                        ▼
┌─────────────────────────────────────────────────────────────────┐
│  publish_github_release                                          │
│  Download all artifacts → Create/update GitHub Release → Upload │
└─────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This flowchart looks quite complex, but when you break it down, it's not that difficult. Many things are like this—they look scary, but when you actually do them, they're just whatever.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Configuration Reference
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Build environment variables&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;CI&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.token }}&lt;/span&gt;
  &lt;span class="na"&gt;ELECTRON_SKIP_BINARY_DOWNLOAD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;    &lt;span class="c1"&gt;# Skip Electron download&lt;/span&gt;
  &lt;span class="na"&gt;PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;# Skip Playwright browser download&lt;/span&gt;
  &lt;span class="na"&gt;npm_config_build_from_source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;# Build native modules from source&lt;/span&gt;
  &lt;span class="na"&gt;VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.prepare_release.outputs.version }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These environment variables are crucial for build speed and correctness: skipping unnecessary binary downloads significantly reduces build time, and &lt;code&gt;build_from_source&lt;/code&gt; ensures native modules compile correctly on the target platform.&lt;/p&gt;

&lt;p&gt;Through this pipeline, HagiCode achieves automated building, verification, and publishing of code-server and OmniRoute across three operating systems, turning what was originally a manual multi-platform publishing process into a fully automated CI/CD process. This can be considered making a troublesome thing less troublesome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The key to designing multi-platform CI/CD pipelines lies in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Centralized version management&lt;/strong&gt;: Generate a unified version number at the start of the pipeline, shared by all downstream steps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separation of build and publish&lt;/strong&gt;: Use &lt;code&gt;fail-fast: false&lt;/code&gt; to ensure a platform failure doesn't affect other platforms, with the publishing stage aggregating all artifacts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform-isolated build scripts&lt;/strong&gt;: Each package maintains its own build logic, while shared toolchain remains package-agnostic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated artifact verification&lt;/strong&gt;: Verify usability immediately after building to avoid discovering problems only after publishing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This solution is not only applicable to code-server and OmniRoute, but can also provide reference for other projects needing multi-platform builds. The build system shared in this article is exactly what we actually stepped on pits and optimized during the development of HagiCode. If you find this solution valuable, it shows our engineering strength is not bad—then HagiCode itself is worth paying attention to.&lt;/p&gt;

&lt;p&gt;After all, people who can automate such troublesome things probably aren't too bad themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode Project Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode Official Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/coder/code-server" rel="noopener noreferrer"&gt;code-server Upstream Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/diegosouzapw/OmniRoute" rel="noopener noreferrer"&gt;OmniRoute Project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions" rel="noopener noreferrer"&gt;GitHub Actions Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If this article helps you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Come to GitHub and give us a Star: &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;github.com/HagiCode-org/site&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Visit the official website to learn more: &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;hagicode.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Watch the official release demo video: &lt;a href="https://www.bilibili.com/video/BV1z4oWB3EpY/" rel="noopener noreferrer"&gt;www.bilibili.com/video/BV1z4oWB3EpY/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;One-click install to try: &lt;a href="https://docs.hagicode.com/installation/docker-compose" rel="noopener noreferrer"&gt;docs.hagicode.com/installation/docker-compose&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Desktop quick install: &lt;a href="https://hagicode.com/desktop/" rel="noopener noreferrer"&gt;hagicode.com/desktop/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Public beta has started, welcome to install and try&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>omniroute</category>
    </item>
    <item>
      <title>Why HagiCode Chose execa for CLI Command Execution</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Tue, 05 May 2026 14:17:03 +0000</pubDate>
      <link>https://dev.to/newbe36524/why-hagicode-chose-execa-for-cli-command-execution-50cd</link>
      <guid>https://dev.to/newbe36524/why-hagicode-chose-execa-for-cli-command-execution-50cd</guid>
      <description>&lt;h1&gt;
  
  
  Why HagiCode Chose execa for CLI Command Execution
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Using &lt;code&gt;child_process&lt;/code&gt; directly in Node.js projects to execute external commands comes with pain points like significant platform differences and inconsistent error handling. This article shares the practical experience of introducing execa in the HagiCode project, including core design decisions and real code examples.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;In Node.js projects, directly using the &lt;code&gt;child_process&lt;/code&gt; module to execute external commands is common practice, but it comes with quite a few issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Significant platform differences&lt;/strong&gt;: Windows &lt;code&gt;.cmd&lt;/code&gt;/&lt;code&gt;.bat&lt;/code&gt; files require special handling, and paths containing spaces need to be wrapped in quotes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inconsistent error handling&lt;/strong&gt;: &lt;code&gt;execFile&lt;/code&gt;, &lt;code&gt;spawn&lt;/code&gt;, and &lt;code&gt;execFileSync&lt;/code&gt; produce error information in varying formats, making unified handling difficult&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tedious stream processing&lt;/strong&gt;: Manual collection and buffering of stdout/stderr streams is required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex timeout and signal handling&lt;/strong&gt;: Extra code is needed to implement command timeout cancellation and process signal handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Hagiscript and Desktop applications within the HagiCode project both need to execute a large number of external CLI commands (npm, node, PowerShell, etc.). Directly using &lt;code&gt;child_process&lt;/code&gt; led to code duplication and high maintenance costs.&lt;/p&gt;

&lt;p&gt;To address these pain points, we made a decision: introduce execa as a unified command execution solution. The impact of this decision turned out to be greater than you might imagine — I'll explain the specifics shortly.&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The approach shared in this article comes from our practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode is an AI coding assistant project that needs to execute a large number of external commands across multiple sub-projects (the Hagiscript scripting engine and the Desktop application). The complexity of supporting multiple languages and platforms is perhaps the direct reason we chose to introduce execa.&lt;/p&gt;

&lt;p&gt;If you find the approach shared in this article valuable, it speaks to our engineering capabilities — and HagiCode itself is worth checking out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why execa?
&lt;/h2&gt;

&lt;p&gt;execa is a mature process execution library that addresses the core problems of &lt;code&gt;child_process&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cross-platform consistency&lt;/strong&gt;: Automatically handles Windows command shims without manually detecting &lt;code&gt;.cmd&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unified error handling&lt;/strong&gt;: Standardized error objects containing exitCode, signal, timedOut, stdout, and stderr&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better API design&lt;/strong&gt;: Supports Promise API, AbortSignal cancellation, and stream processing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;: Maintains argument boundaries, avoiding command injection risks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are exactly the features we needed during HagiCode development. Hagiscript needs to execute npm commands across different platforms, and Desktop needs to call PowerShell and various development tools. execa's cross-platform consistency significantly reduced our platform-specific code. After all, who wants to write special handling code for every platform?&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Design Decisions
&lt;/h2&gt;

&lt;p&gt;Both projects' implementations use an &lt;strong&gt;internal wrapper layer&lt;/strong&gt; rather than calling execa directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Hagiscript's unified executor&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runCommand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CommandRunner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;execa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* normalized options */&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* normalized result */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Reasons&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintain domain-specific error types (e.g., &lt;code&gt;NpmCommandError&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Enable injecting mock executors during testing&lt;/li&gt;
&lt;li&gt;Unify error handling and logging&lt;/li&gt;
&lt;li&gt;Make it easy to replace the underlying implementation in the future&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Argument Boundary Protection
&lt;/h3&gt;

&lt;p&gt;Both implementations emphasize &lt;strong&gt;argument arrays&lt;/strong&gt; over shell strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Correct: clear argument boundaries&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@scope/package@1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// Wrong: prone to injection risks&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;execa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`npm install @scope/package@1.0.0`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids security issues related to argument quoting, escaping, and injection. In HagiCode, we frequently handle user-supplied inputs like package names and script names as parameters. Using argument arrays effectively prevents command injection. When it comes to security, once something goes wrong, it's a big problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hagiscript's Solution
&lt;/h2&gt;

&lt;p&gt;HagiCode's Hagiscript sub-project created a &lt;code&gt;runtime/command-launch.ts&lt;/code&gt; module that provides:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unified executor&lt;/strong&gt;: The &lt;code&gt;runCommand&lt;/code&gt; function wraps execa&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standardized result&lt;/strong&gt;: &lt;code&gt;CommandResult&lt;/code&gt; interface&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standardized error&lt;/strong&gt;: &lt;code&gt;CommandExecutionError&lt;/code&gt; class&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compatibility helpers&lt;/strong&gt;: &lt;code&gt;normalizeCommandPath&lt;/code&gt;, &lt;code&gt;requiresShellLaunch&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CommandResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;exitCode&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;timedOut&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommandExecutionError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CommandFailureContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This abstraction allows Hagiscript to handle all external commands uniformly, whether installing npm dependencies or executing scripts with node. With a unified interface, the code really does flow much more smoothly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Desktop's Solution
&lt;/h2&gt;

&lt;p&gt;HagiCode's Desktop sub-project created a &lt;code&gt;utils/cli-executor.ts&lt;/code&gt; module that provides:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Execution options&lt;/strong&gt;: &lt;code&gt;CliExecutorOptions&lt;/code&gt; supports timeout, cancellation, and environment variables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result classification&lt;/strong&gt;: &lt;code&gt;CliExecutionResult&lt;/code&gt; includes success/failure status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stream processing&lt;/strong&gt;: &lt;code&gt;executeCliStreaming&lt;/code&gt; supports real-time output callbacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error classification&lt;/strong&gt;: &lt;code&gt;CliFailureKind&lt;/code&gt; distinguishes between exit, timeout, cancellation, and other failure types
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;executeCli&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CliExecutorOptions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CliExecutionResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;executeCliStreaming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CliExecutorOptions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CliExecutionResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Desktop application needs to display command execution progress in the UI, which is where the streaming feature comes in handy. Users can see the output of &lt;code&gt;npm install&lt;/code&gt; in real time rather than waiting until the command finishes. Once you've experienced it, there's no going back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Executing Commands in Hagiscript
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../runtime/command-launch.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Simple execution&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--version&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 'v20.0.0'&lt;/span&gt;

&lt;span class="c1"&gt;// Execution with options&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;installResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/project/path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Executing Commands in Desktop
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;executeCli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;executeCliStreaming&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./utils/cli-executor.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Buffered execution&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;executeCli&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;projectPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Streaming execution&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;executeCliStreaming&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;onOutput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Error Handling
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid-package&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;CommandExecutionError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Command failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Exit code:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exitCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Stderr:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unified error handling allows us to provide a better user experience in HagiCode. For example, when an npm installation fails, we can extract the specific error message and display it to the user instead of showing a generic "command execution failed" message. Seeing the specific error at least tells the user where the problem lies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Strategy
&lt;/h2&gt;

&lt;p&gt;Both projects support &lt;strong&gt;dependency injection&lt;/strong&gt; for easier testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Production code&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;installPackage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;runCommand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;defaultRunCommand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;runCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Test code&lt;/span&gt;
&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;installs package&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mockRunCommand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;mockResolvedValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;installed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;exitCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;installPackage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test-pkg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mockRunCommand&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockRunCommand&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test-pkg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design makes HagiCode's tests more reliable and faster. We don't need to actually execute npm commands in tests — we only need to mock the executor to return expected results. Faster tests naturally lead to a better development experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;p&gt;Based on HagiCode's practice, we've summarized the following best practices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keep arguments separated&lt;/strong&gt;: Always pass the command and arguments as separate array elements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use shell mode sparingly&lt;/strong&gt;: Only use &lt;code&gt;shell: true&lt;/code&gt; when necessary, such as when piping or redirection is needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle timeouts&lt;/strong&gt;: Set &lt;code&gt;timeoutMs&lt;/code&gt; for commands that may hang&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buffer size&lt;/strong&gt;: Consider setting &lt;code&gt;maxBuffer&lt;/code&gt; for commands with large output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows paths&lt;/strong&gt;: execa automatically handles &lt;code&gt;.cmd&lt;/code&gt; shims — no manual detection needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cancellation&lt;/strong&gt;: Use &lt;code&gt;AbortSignal&lt;/code&gt; instead of manual &lt;code&gt;kill()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error classification&lt;/strong&gt;: Distinguish between process startup failure, execution failure, timeout, cancellation, and other scenarios&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are all pitfalls we've encountered during actual development. Hopefully they can save you some detours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong: string concatenation may allow injection&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;execa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`npm install &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Correct: argument array&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;execa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// Wrong: ignoring timeout&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;execa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heavy-package&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// Correct: set timeout&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;execa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heavy-package&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Wrong: assuming exit code is 0&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;execa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// Correct: check for failure&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;execa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Handle failure&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These pitfalls are hard-won lessons. After all, who hasn't stepped on a few landmines in production?&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;After introducing execa, the HagiCode project saw significant improvements in both code quality and maintainability for command execution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-platform consistency&lt;/strong&gt;: No more writing special handling code for Windows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unified error handling&lt;/strong&gt;: Structured error messages make display and analysis easier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better testability&lt;/strong&gt;: Command execution can be easily mocked through dependency injection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More secure argument handling&lt;/strong&gt;: Using argument arrays avoids injection risks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you also need to execute external commands in a Node.js project, we highly recommend giving execa a try. The approach shared in this article was refined through real-world pitfalls and optimizations during HagiCode development. We hope you find it helpful.&lt;/p&gt;

&lt;p&gt;Good tools deserve wider recognition.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/sindresorhus/execa" rel="noopener noreferrer"&gt;execa Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/api/child_process.html" rel="noopener noreferrer"&gt;Node.js child_process Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode Official Website&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this article was helpful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give us a Star on GitHub: &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;github.com/HagiCode-org/site&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Visit our website to learn more: &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;hagicode.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Watch a 30-minute hands-on demo: &lt;a href="https://www.bilibili.com/video/BV1pirZBuEzq/" rel="noopener noreferrer"&gt;www.bilibili.com/video/BV1pirZBuEzq/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Quick install with one click: &lt;a href="https://docs.hagicode.com/installation/docker-compose" rel="noopener noreferrer"&gt;docs.hagicode.com/installation/docker-compose&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Desktop app quick install: &lt;a href="https://hagicode.com/desktop/" rel="noopener noreferrer"&gt;hagicode.com/desktop/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Original Article &amp;amp; License
&lt;/h2&gt;

&lt;p&gt;Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.&lt;br&gt;
This article was created with AI assistance and reviewed by the author before publication.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author: &lt;a href="https://www.newbe.pro" rel="noopener noreferrer"&gt;newbe36524&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Original URL: &lt;a href="https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-04-28-why-hagicode-chose-execa-for-cli-execution%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-04-28-why-hagicode-chose-execa-for-cli-execution%2F&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>execa</category>
      <category>hagicode</category>
    </item>
    <item>
      <title>Implementing Auto-Retry for Agent CLIs like Claude Code and Codex</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Sat, 18 Apr 2026 09:14:33 +0000</pubDate>
      <link>https://dev.to/newbe36524/implementing-auto-retry-for-agent-clis-like-claude-code-and-codex-18a2</link>
      <guid>https://dev.to/newbe36524/implementing-auto-retry-for-agent-clis-like-claude-code-and-codex-18a2</guid>
      <description>&lt;h1&gt;
  
  
  Implementing Auto-Retry for Agent CLIs like Claude Code and Codex
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Auto-retry might seem like a simple switch, but in real-world engineering, it's anything but. Hello everyone, I'm Yu Kun, creator of HagiCode. Today, let's skip the fluff and talk about how auto-retry for Agent CLIs like Claude Code and Codex should actually be implemented—to handle exceptions properly without spiraling into endless retry loops.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;If you've been working with AI programming recently, you've likely encountered this: tasks don't fail immediately—they break halfway through.&lt;/p&gt;

&lt;p&gt;With ordinary HTTP requests, you often just retry with some exponential backoff. But Agent CLIs are different. Tools like Claude Code and Codex typically execute in streaming fashion, pushing output in chunks, while binding to threads, sessions, or resume tokens. In other words, it's not just "did this request fail" but rather:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the content already output still valid&lt;/li&gt;
&lt;li&gt;Can the current context continue running&lt;/li&gt;
&lt;li&gt;Should this failure auto-recover&lt;/li&gt;
&lt;li&gt;If recovering, how long to wait, what to send, whether to reuse the original context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many teams building this for the first time instinctively write the simplest version: retry on error. That's natural, but once it's in the project, problems start popping up one after another:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some clearly transient errors get treated as final failures&lt;/li&gt;
&lt;li&gt;Some errors not worth retrying get replayed repeatedly&lt;/li&gt;
&lt;li&gt;Requests with threads and without get treated identically&lt;/li&gt;
&lt;li&gt;Unbounded backoff strategies pound the backend with self-inflicted load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;HagiCode has stepped in these pits while integrating multiple Agent CLIs. Especially on the Codex side, the initially exposed problem was that certain reconnect messages weren't recognized as retryable terminal states, so existing recovery mechanisms never got a chance to kick in. Basically, the system didn't lack auto-retry—it failed to recognize "this is worth retrying."&lt;/p&gt;

&lt;p&gt;So the core point of this article is clear: &lt;strong&gt;Auto-retry is not a button, but a layered design.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution shared here comes from our real practice in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode's mission isn't just to hook up some model and call it done—it's to unify streaming messages, tool calls, failure recovery, and session context across multiple Agent CLIs into a maintainable execution model.&lt;/p&gt;

&lt;p&gt;One of my main concerns is how to make AI programming actually work in production environments. Writing demos isn't hard; turning demos into something teams will use long-term is. HagiCode takes auto-retry seriously not because it looks fancy, but because if long-running, streaming, resumable CLI execution isn't stable, users see an unreliable wrapper that drops connections mid-task, not an intelligent assistant.&lt;/p&gt;

&lt;p&gt;If you want to check out the project first, here are two entry points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;github.com/HagiCode-org/site&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Official site: &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;hagicode.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Taking it a step further, HagiCode is now on Steam. If you're on Steam, feel free to wishlist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://store.steampowered.com/app/4625540/Hagicode/" rel="noopener noreferrer"&gt;Steam Store Page (Wishlist / Details)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Agent CLI Auto-Retry is Harder Than Ordinary Retry
&lt;/h2&gt;

&lt;p&gt;This is a practical question—let's jump to the conclusion: The difficulty with Agent CLI auto-retry isn't "wait a few seconds and try again," but "can we continue within the original context?"&lt;/p&gt;

&lt;p&gt;Think of it like a long conversation. Ordinary API retry is like a busy line—redial. Agent CLI retry is like the other person's signal cutting mid-sentence. You have to decide: call back? Start over? Do they remember where we left off? These aren't the same problem at all.&lt;/p&gt;

&lt;p&gt;Specifically, four challenges are most typical.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. It's Streaming
&lt;/h3&gt;

&lt;p&gt;Once output is sent to the user, you can't quietly swallow failures and retry like with ordinary requests. Because that initial content was already seen, improper replay leads to duplicate text, confused state, and scrambled tool call lifecycles. This isn't magic—it's engineering.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. It Binds Session Context
&lt;/h3&gt;

&lt;p&gt;Providers like Codex bind to threads; Claude Code-type implementations have continuation targets or equivalent resume context. Real auto-retry prerequisites aren't just "this error looks like transient failure," but also "does this execution still have a medium to continue?"&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Not All Errors Are Worth Retrying
&lt;/h3&gt;

&lt;p&gt;Network jitter, SSE idle timeout, upstream transient failures—usually worth a try. But authentication failure, lost context, or providers without resume capability? Continued retrying isn't recovery, it's noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. It Needs Boundaries
&lt;/h3&gt;

&lt;p&gt;Infinite auto-retry is almost always wrong. One stable engineering principle is: failure recovery must have boundaries. The system must know: max attempts, spacing between attempts, when to stop and admit defeat.&lt;/p&gt;

&lt;p&gt;Because of these characteristics, HagiCode didn't implement auto-retry as a few &lt;code&gt;try/catch&lt;/code&gt; lines in some provider—we extracted it as shared capability. Engineering problems need engineering solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  HagiCode's Approach: Extract Retry from Provider
&lt;/h2&gt;

&lt;p&gt;HagiCode's current real implementation can be compressed to one sentence:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared layer uniformly manages retry flow; specific Providers only answer two questions: Is this terminal state worth retrying? Can current context continue?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This isn't complex, but it's critical. Once responsibilities are separated, Claude Code, Codex, and even other Agent CLIs can all reuse the same skeleton. Models change, tools change, workflows upgrade, but the engineering foundation remains.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Unified Coordinator Manages Retry Loop
&lt;/h3&gt;

&lt;p&gt;The core implementation fragment looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProviderErrorAutoRetryCoordinator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;IAsyncEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CliMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ProviderErrorAutoRetrySettings&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IAsyncEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CliMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;executeAttemptAsync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;canRetryInSameContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;delayAsync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CliMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;isRetryableTerminalMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnumeratorCancellation&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;normalizedSettings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ProviderErrorAutoRetrySettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;retrySchedule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;normalizedSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Enabled&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;normalizedSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetRetrySchedule&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;attemptPrompt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
                &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;
                &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ProviderErrorAutoRetrySettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContinuationPrompt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="n"&gt;CliMessage&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;terminalFailure&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;executeAttemptAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attemptPrompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                               &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithCancellation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isRetryableTerminalMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;terminalFailure&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;terminalFailure&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;retrySchedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;canRetryInSameContext&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;terminalFailure&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;delayAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retrySchedule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code does something simple but powerful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Intermediate failures aren't directly passed through; coordinator first judges if recovery is possible&lt;/li&gt;
&lt;li&gt;Only when retry budget is exhausted does final failure return to upper layers&lt;/li&gt;
&lt;li&gt;From round 2 onward, don't send original prompt—send unified continuation prompt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why I keep emphasizing: auto-retry isn't simply "request again." It's not patching an exception branch—it's managing an execution lifecycle. Sounds product-manager-ish, but that's how it works in engineering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Snapshot Retry Strategy
&lt;/h3&gt;

&lt;p&gt;Another easily overlooked issue: Who decides whether this request enables auto-retry?&lt;/p&gt;

&lt;p&gt;HagiCode's answer: Don't rely on "current global configuration"—snapshot the strategy and let it travel with the request. This way, session queuing, message persistence, execution forwarding, provider adaptation won't lose the strategy. One success isn't a system; sustained success is.&lt;/p&gt;

&lt;p&gt;Core structure simplifies to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;ProviderErrorAutoRetrySnapshot&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;DefaultStrategy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;Enabled&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Strategy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DefaultStrategy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;ProviderErrorAutoRetrySnapshot&lt;/span&gt; &lt;span class="nf"&gt;Normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ProviderErrorAutoRetrySnapshot&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Strategy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;DefaultStrategy&lt;/span&gt;
                &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then map to settings objects actually consumed by providers at execution time. The value is direct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Business layer decides "whether to retry"&lt;/li&gt;
&lt;li&gt;Runtime decides "how to retry"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each manages its own concern. Many problems aren't impossible, just not properly costed. Snapshotting strategy essentially calculates costs upfront.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: Provider Only Does Terminal and Context Determination
&lt;/h3&gt;

&lt;p&gt;At specific Claude Code or Codex provider level, responsibilities are actually thin. Think of it as enhancement, not replacement.&lt;/p&gt;

&lt;p&gt;Take Codex—it essentially only needs to provide three things when integrating the shared coordinator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ProviderErrorAutoRetryCoordinator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                   &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProviderErrorAutoRetry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;retryPrompt&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ExecuteCodexAttemptAsync&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;
                   &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolvedThreadId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                   &lt;span class="n"&gt;DelayAsync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;IsRetryableTerminalFailure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll find truly Provider-specific judgments are only two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IsRetryableTerminalFailure&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;canRetryInSameContext&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Codex checks if thread can continue; Claude Code checks if continuation target exists. Backoff strategy, retry count, subsequent prompts—none of these should be reinvented by each Provider.&lt;/p&gt;

&lt;p&gt;Once this layer is extracted, HagiCode's cost to integrate more CLIs drops significantly. You don't duplicate the entire retry state machine—just plug in "this provider's boundary conditions." Fast doesn't mean stable; handling doesn't mean handling well; runnable doesn't means maintainable.&lt;/p&gt;

&lt;h2&gt;
  
  
  An Easy Mistake: Don't Treat All Errors as Retryable
&lt;/h2&gt;

&lt;p&gt;In this analysis, what's most worth calling out separately isn't "how to implement retry" but "how to avoid wrong retry."&lt;/p&gt;

&lt;p&gt;The initial problem entry point was Codex missing recognition of a reconnect message. Intuitively, many would choose minimal fix: add another string prefix to whitelist. This isn't wrong per se, but it's more like a demo-era solution, not a long-term maintainable one.&lt;/p&gt;

&lt;p&gt;From current HagiCode implementation, the system has moved toward more stable direction. It no longer focuses on specific literal strings but uniformly hands recoverable terminal states to shared coordinator. Benefits are obvious:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Won't completely break from minor text copy changes&lt;/li&gt;
&lt;li&gt;Test coverage can focus on "terminal state envelope" rather than single hardcoded text&lt;/li&gt;
&lt;li&gt;Same provider's retry logic stays more consistent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, set a boundary: more generic doesn't mean more permissive. &lt;strong&gt;If current context cannot continue, even if error looks like transient failure, don't blindly replay.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is critical. What's truly reassuring isn't that it occasionally works, but that it's reliable most of the time. If a process requires experts to maintain, it's far from mainstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Most Worthwhile Practical Lessons
&lt;/h2&gt;

&lt;p&gt;Let's wrap this up at the practical level. If you're implementing similar capability in your project, I most recommend guarding these three principles.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Retry Budget Must Have Boundaries
&lt;/h3&gt;

&lt;p&gt;HagiCode's current default backoff rhythm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10 seconds&lt;/li&gt;
&lt;li&gt;20 seconds&lt;/li&gt;
&lt;li&gt;60 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This rhythm might not fit all systems, but "having boundaries" must be kept. Otherwise, auto-retry quickly transforms from recovery mechanism into disaster amplifier. Don't rush to name it grand—first see if it survives two iterations in your team.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Continuation Prompt Should Be Unified
&lt;/h3&gt;

&lt;p&gt;The project uses fixed continuation prompts, letting subsequent attempts explicitly take "continue current context" path rather than initiating a fresh complete request. This isn't flashy, but it's indispensable in real projects. Many capabilities look like magic, but unpacked they're just polished engineering workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Both Shared Library and Adapter Layer Need Mirror Tests
&lt;/h3&gt;

&lt;p&gt;I want to emphasize this. Many teams write tests in shared runtime and call it good enough. It's not.&lt;/p&gt;

&lt;p&gt;What makes me confident about HagiCode is both layers have test coverage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shared Provider tests "whether auto-resume actually occurred"&lt;/li&gt;
&lt;li&gt;Adapter layer tests "whether final errors and streaming messages were corrupted"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I additionally ran two related test suites this time—all 31 cases passed. This result itself doesn't prove perfect design, but it shows at least one thing: current auto-retry isn't a paper plan—it's capability constrained by both code and tests. Talk is cheap. Show me the code. Fits perfectly here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;If we compress this article to one sentence:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-retry for Agent CLIs like Claude Code and Codex is best implemented not as local tricks inside some Provider, but as a combination of shared coordinator + strategy snapshot + context determination + mirror tests.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Benefits are very real:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logic written once, reused by multiple Providers&lt;/li&gt;
&lt;li&gt;Whether request allows retrying can stably follow execution chain&lt;/li&gt;
&lt;li&gt;Continue with context, stop without context&lt;/li&gt;
&lt;li&gt;Frontend sees stable completion or failure states, not abandoned intermediate noise&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This solution was polished through HagiCode's real integration of multiple Agent CLIs. Who says AI-assisted programming isn't the new pair programming? Models help you start, complete, diverge—but what ultimately determines experience ceiling is context, workflow, and constraints.&lt;/p&gt;

&lt;p&gt;If this helped you, also feel free to check out HagiCode's public entry points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;github.com/HagiCode-org/site&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Official site: &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;hagicode.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;30-minute demo: &lt;a href="https://www.bilibili.com/video/BV1pirZBuEzq/" rel="noopener noreferrer"&gt;www.bilibili.com/video/BV1pirZBuEzq/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Desktop install: &lt;a href="https://hagicode.com/desktop/" rel="noopener noreferrer"&gt;hagicode.com/desktop/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Steam: &lt;a href="https://store.steampowered.com/app/4625540/Hagicode/" rel="noopener noreferrer"&gt;Steam Store Page (Wishlist / Details)&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;HagiCode is now on Steam—this isn't vaporware, link's right here. If you're on Steam, wishlist it and click through. More direct than me saying ten sentences here.&lt;/p&gt;

&lt;p&gt;That's it for now—see you in real projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;HagiCode project homepage: &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;https://hagicode.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;HagiCode GitHub repo: &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;https://github.com/HagiCode-org/site&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Official demo video: &lt;a href="https://www.bilibili.com/video/BV1pirZBuEzq/" rel="noopener noreferrer"&gt;https://www.bilibili.com/video/BV1pirZBuEzq/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Desktop install guide: &lt;a href="https://hagicode.com/desktop/" rel="noopener noreferrer"&gt;https://hagicode.com/desktop/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Original Article &amp;amp; License
&lt;/h2&gt;

&lt;p&gt;Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.&lt;br&gt;
This article was created with AI assistance and reviewed by the author before publication.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author: &lt;a href="https://www.newbe.pro" rel="noopener noreferrer"&gt;newbe36524&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Original URL: &lt;a href="https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-02-11-agent-cli-automatic-retry%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-02-11-agent-cli-automatic-retry%2F&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agentcli</category>
      <category>claudecode</category>
      <category>codex</category>
      <category>hagicode</category>
    </item>
    <item>
      <title>SQLite Sharding in Practice: A Deep Comparison of Three Sharding Strategies</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Fri, 17 Apr 2026 05:53:37 +0000</pubDate>
      <link>https://dev.to/newbe36524/sqlite-sharding-in-practice-a-deep-comparison-of-three-sharding-strategies-2a0o</link>
      <guid>https://dev.to/newbe36524/sqlite-sharding-in-practice-a-deep-comparison-of-three-sharding-strategies-2a0o</guid>
      <description>&lt;h1&gt;
  
  
  SQLite Sharding in Practice: A Deep Comparison of Three Sharding Strategies
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;When single-file SQLite hits concurrency bottlenecks, how do we break through? This article shares three SQLite sharding approaches from different scenarios in the HagiCode project, helping you understand how to choose the right sharding strategy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hello everyone, I'm Yu Kun, producer of HagiCode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;When building high-performance applications, single-file SQLite databases encounter very real problems. As user volume and data grow, these issues start lining up at your door:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write operations begin queuing, response times visibly increase&lt;/li&gt;
&lt;li&gt;Query performance declines as data grows&lt;/li&gt;
&lt;li&gt;Frequent "database is locked" errors during multi-threaded access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many people's first reaction is: should we just migrate to PostgreSQL or MySQL? While this approach can solve the problem, deployment complexity rises sharply. Is there a lighter-weight solution?&lt;/p&gt;

&lt;p&gt;The answer is: sharding. Ultimately, engineering problems must be solved with engineering methods. By distributing data across multiple SQLite files, you can significantly improve concurrency and query performance while maintaining SQLite's lightweight characteristics.&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The approaches shared in this article come from our practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. As an AI code assistant project, HagiCode needs to handle large volumes of conversation messages, state persistence, and event history records. It was in solving these real-world problems that we summarized three different sharding approaches for different scenarios.&lt;/p&gt;

&lt;p&gt;To do good work, one must first sharpen one's tools—but how to use these "tools" depends on the specific "work" at hand.&lt;/p&gt;

&lt;p&gt;Our code repository is at &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;github.com/HagiCode-org/site&lt;/a&gt;—friends who are interested are welcome to dive deeper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview of Three Sharding Approaches
&lt;/h2&gt;

&lt;p&gt;Through analysis of the HagiCode codebase, we discovered three SQLite sharding approaches for different business scenarios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Session Message Sharded Storage&lt;/strong&gt;: AI conversation message storage, characterized by high-frequency writes and session-based isolated queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orleans Grain Sharded Storage&lt;/strong&gt;: Distributed framework state persistence, characterized by cross-node access requiring deterministic routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hero History Sharded Storage&lt;/strong&gt;: Gamification system historical event records, characterized by event sourcing requiring migration compatibility&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Although the business scenarios differ, all three follow the same core design principles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic routing&lt;/strong&gt;: Directly calculate shards from business IDs, no metadata tables needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparent access&lt;/strong&gt;: Upper layers operate through unified interfaces, unaware of sharding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent storage&lt;/strong&gt;: Each shard is a completely independent SQLite file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency optimization&lt;/strong&gt;: WAL mode + busy_timeout reduces lock contention&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many people ask: Why not build a universal sharding solution? This is a very practical question. Here's our conclusion: In engineering, there are no universal solutions, only approaches that best fit the current business scenario. Next, we'll deeply compare the specific implementations of these three approaches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharding Strategy Comparison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Shard Count and Naming Rules
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Session Message&lt;/th&gt;
&lt;th&gt;Orleans Grain&lt;/th&gt;
&lt;th&gt;Hero History&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Shard Count&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;256 (16²)&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Naming Rule&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hexadecimal (00-ff)&lt;/td&gt;
&lt;td&gt;Decimal (00-99)&lt;/td&gt;
&lt;td&gt;Decimal (0-9)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage Directory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DataDir/messages/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DataDir/orleans/grains/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DataDir/hero-history/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Filename Pattern&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{shard}.db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;grains-{shard}.db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{shard}.db&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why such significant differences in shard count? This depends on business characteristics. In other words, models will change, tools will evolve, workflows will upgrade, but the engineering fundamentals remain: you must first understand what problem you're solving.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session Message&lt;/strong&gt; uses 256 shards because conversation messages have the highest write frequency, requiring more shards to distribute load&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orleans Grain&lt;/strong&gt; uses 100 shards, balancing concurrency performance with management complexity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hero History&lt;/strong&gt; uses only 10 shards because historical events have lower write frequency and migration costs must be considered&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Routing Algorithm Differences
&lt;/h3&gt;

&lt;p&gt;Routing algorithms are the core of sharding approaches, determining how data distributes across shards. The three approaches use different routing strategies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Session Message: GUID last two digits hexadecimal&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"N"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToLowerInvariant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;[^&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;..];&lt;/span&gt;  &lt;span class="c1"&gt;// Take last two hexadecimal characters&lt;/span&gt;

&lt;span class="c1"&gt;// Orleans Grain: Extract digits and take last two digits modulo&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;digits&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ExtractDigits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grainId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Extract all digits&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lastTwoDigits&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;[^&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;[^&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;lastTwoDigits&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt; &lt;span class="n"&gt;shardCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Hero History: Last character ASCII value modulo&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;heroId&lt;/span&gt;&lt;span class="p"&gt;[^&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Design Logic Analysis&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session Message&lt;/strong&gt; IDs are GUIDs; after converting to hexadecimal and taking the last two digits, you get evenly distributed 256 shards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orleans Grain&lt;/strong&gt; ID formats are inconsistent, possibly containing both letters and numbers, so all digits are extracted before modulo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hero History&lt;/strong&gt; IDs are strings; directly using the last character's ASCII value modulo is simple but distribution may not be even enough&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Point&lt;/strong&gt;: Regardless of which algorithm is used, you must ensure the same ID always maps to the same shard. This is the most basic requirement in distributed systems—otherwise, data inconsistency results. Ultimately, unstable routing means all effort is wasted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initialization Strategy Differences
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Session Message&lt;/th&gt;
&lt;th&gt;Orleans Grain&lt;/th&gt;
&lt;th&gt;Hero History&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Initialization Timing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;On-demand lazy loading&lt;/td&gt;
&lt;td&gt;Startup full parallel initialization&lt;/td&gt;
&lt;td&gt;On-demand lazy loading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency Control&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lazy prevents duplicate initialization&lt;/td&gt;
&lt;td&gt;Parallel.ForEachAsync&lt;/td&gt;
&lt;td&gt;Lazy prevents duplicate initialization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why does Orleans Grain choose full initialization at startup?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because Orleans is a distributed framework, Grains may be scheduled to any node. If shard files are discovered missing only at runtime, requests will fail. Full initialization at startup extends startup time but ensures runtime stability. Getting it running is just the beginning; keeping it maintainable is real skill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lazy Loading Advantages&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;For Session Message and Hero History, lazy loading reduces startup time—files and Schema are created only when actually accessing a specific shard. Using &lt;code&gt;Lazy&amp;lt;Task&amp;gt;&lt;/code&gt; prevents race conditions during concurrent initialization. This design looks simple, but saves a lot of unnecessary trouble in real projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schema Design Characteristics
&lt;/h3&gt;

&lt;p&gt;The three approaches' Schema designs reflect their respective business characteristics:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session Message&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supports Event Sourcing pattern (event table + snapshot table)&lt;/li&gt;
&lt;li&gt;Includes message content block sub-table (MessageContentBlocks)&lt;/li&gt;
&lt;li&gt;Has compression and compression flag fields, supporting future optimization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Orleans Grain&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimalist design: single table GrainState&lt;/li&gt;
&lt;li&gt;JSON serialization for state storage&lt;/li&gt;
&lt;li&gt;ETag optimistic concurrency control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hero History&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Timeline query optimization indexes&lt;/li&gt;
&lt;li&gt;DedupeKey unique constraint prevents duplication&lt;/li&gt;
&lt;li&gt;Supports multiple event types and statuses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From these designs, we can see Schema design should closely fit business requirements, not pursue generality. Orleans Grain's simple design exists because it only needs to store serialized state, without complex query capabilities. This isn't magic—it's engineering. Don't rush to give things grand names—first see if they can survive two iterations in the team.&lt;/p&gt;

&lt;h3&gt;
  
  
  Concurrency Configuration Comparison
&lt;/h3&gt;

&lt;p&gt;All three approaches use the same SQLite concurrency optimization configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;journal_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;WAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;-- Write-ahead logging mode&lt;/span&gt;
&lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;synchronous&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NORMAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;-- Reduce persistence overhead&lt;/span&gt;
&lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;busy_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;-- 5 second busy wait&lt;/span&gt;
&lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;foreign_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;-- Foreign key constraints&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;WAL Mode Advantages&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Traditional rollback journal mode produces lock contention during writes, while WAL mode allows concurrent reads and writes. This can significantly improve performance in large data volume scenarios. Many people don't know this configuration—actually, it's more important than you think.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;synchronous=NORMAL Trade-off&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Setting to FULL guarantees maximum safety but significantly reduces performance. NORMAL mode achieves balance between safety and performance, making it the right choice for most applications. Don't struggle with this configuration too long—NORMAL is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Choose a Sharding Strategy
&lt;/h2&gt;

&lt;p&gt;Based on analysis of HagiCode's three approaches, we can summarize this decision matrix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;High throughput scenarios → More shards (e.g., Message uses 256)
Simple maintainability     → Fewer shards (e.g., Hero History uses 10)
Mostly numeric IDs         → Modulo algorithm (Orleans Grain)
Mostly GUIDs              → Hexadecimal suffix (Session Message)
String IDs                → ASCII modulo (Hero History)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Experience Values for Shard Count Selection&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too few (&amp;lt; 10): Limited concurrency improvement, little sharding benefit&lt;/li&gt;
&lt;li&gt;Too many (&amp;gt; 1000): Complex file management, high connection pool overhead&lt;/li&gt;
&lt;li&gt;Experience value: 10-100 shards suitable for most scenarios&lt;/li&gt;
&lt;li&gt;Extremely high concurrency scenarios: Consider 256 shards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This might look exciting in demos, but once you're in production, every cost must be calculated carefully. Many problems aren't impossible—just haven't had their costs properly calculated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Guide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Implement Standardized Shard Router
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IShardResolver&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TId&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;ResolveShardKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TId&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Hexadecimal sharding (for GUIDs)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HexSuffixShardResolver&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IShardResolver&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;_suffixLength&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;HexSuffixShardResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;suffixLength&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_suffixLength&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;suffixLength&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;ResolveShardKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToLowerInvariant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;[^&lt;/span&gt;&lt;span class="n"&gt;_suffixLength&lt;/span&gt;&lt;span class="p"&gt;..];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Numeric modulo sharding (for pure numeric IDs)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NumericModuloShardResolver&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IShardResolver&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;_shardCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;NumericModuloShardResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;shardCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_shardCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shardCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;ResolveShardKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt; &lt;span class="n"&gt;_shardCount&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"D2"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Unified Connection Factory Pattern
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShardedConnectionFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ConcurrentDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Lazy&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_initializationTasks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;TOptions&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IShardSchemaInitializer&lt;/span&gt; &lt;span class="n"&gt;_initializer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;ShardedConnectionFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TOptions&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IShardSchemaInitializer&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_initializer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;shardKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;connectionString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;BuildConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shardKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Use Lazy&amp;lt;Task&amp;gt; to prevent concurrent initialization&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;initTask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_initializationTasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetOrAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Lazy&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;InitializeShardAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;initTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;CreateDbContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InitializeShardAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_initializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;BuildConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;shardKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shardPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseDirectory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;shardKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.db"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;$"Data Source=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;shardPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;TDbContext&lt;/span&gt; &lt;span class="nf"&gt;CreateDbContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Create DbContext based on specific ORM&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Activator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TDbContext&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;TDbContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Schema Initialization Best Practices
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SqliteShardInitializer&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IShardSchemaInitializer&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SqliteConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Concurrency optimization configuration&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""
&lt;/span&gt;            &lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;journal_mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="n"&gt;WAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;synchronous&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NORMAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;busy_timeout&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="m"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;foreign_keys&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="s"&gt;""");
&lt;/span&gt;
        &lt;span class="c1"&gt;// Create table structure&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""
&lt;/span&gt;            &lt;span class="n"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;EXISTS&lt;/span&gt; &lt;span class="nf"&gt;Entities&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt; &lt;span class="n"&gt;PRIMARY&lt;/span&gt; &lt;span class="n"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt; &lt;span class="n"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;UpdatedAt&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt; &lt;span class="n"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Data&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt; &lt;span class="n"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;ETag&lt;/span&gt; &lt;span class="n"&gt;TEXT&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="s"&gt;""");
&lt;/span&gt;
        &lt;span class="c1"&gt;// Create indexes&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""
&lt;/span&gt;            &lt;span class="n"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;IX_Entities_CreatedAt&lt;/span&gt;
            &lt;span class="n"&gt;ON&lt;/span&gt; &lt;span class="nf"&gt;Entities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="n"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;IX_Entities_UpdatedAt&lt;/span&gt;
            &lt;span class="n"&gt;ON&lt;/span&gt; &lt;span class="nf"&gt;Entities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UpdatedAt&lt;/span&gt; &lt;span class="n"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="s"&gt;""");
&lt;/span&gt;    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Considerations
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Routing Stability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Routing algorithms must guarantee the same ID always maps to the same shard. Avoid using random or time-related calculations, and don't introduce mutable parameters in the algorithm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Shard Count Selection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Shard count should be determined in the design phase; later modification is very difficult. Consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current and future concurrency levels&lt;/li&gt;
&lt;li&gt;Management cost per individual shard&lt;/li&gt;
&lt;li&gt;Data migration complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Migration Considerations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Hero History approach demonstrates a complete migration path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build new sharded storage infrastructure&lt;/li&gt;
&lt;li&gt;Implement migration service to copy main database data to shards&lt;/li&gt;
&lt;li&gt;Verify query compatibility after migration&lt;/li&gt;
&lt;li&gt;Switch read/write paths to shards&lt;/li&gt;
&lt;li&gt;Clean up old main database tables&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Design sharding approaches with future migration needs in mind. Talk is cheap. Show me the code—but code alone isn't enough; you need a complete migration path. One success doesn't make a system; sustained success does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Monitoring and Operations&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monitor each shard's size distribution, detect data skew promptly&lt;/li&gt;
&lt;li&gt;Set up alerts for shard hotspots, avoid single shard becoming bottleneck&lt;/li&gt;
&lt;li&gt;Regularly check WAL file sizes, prevent excessive disk space usage&lt;/li&gt;
&lt;li&gt;Establish shard health check mechanisms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Test Coverage&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test edge cases (empty ID, special characters, overly long ID)&lt;/li&gt;
&lt;li&gt;Verify routing determinism, ensure same ID always maps to same shard&lt;/li&gt;
&lt;li&gt;Concurrent write stress testing, verify lock contention is effectively mitigated&lt;/li&gt;
&lt;li&gt;Migration testing, ensure data integrity and consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By comparing three SQLite sharding approaches in the HagiCode project, we can see:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No universal solution&lt;/strong&gt;: Different business scenarios require different sharding strategies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Core principles are universal&lt;/strong&gt;: Deterministic routing, transparent access, independent storage, concurrency optimization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design for the future&lt;/strong&gt;: Consider migration paths and operational costs&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your project is using SQLite and starting to encounter concurrency bottlenecks, I hope this article provides some ideas. There's no need to rush into migrating to heavyweight databases—sometimes an appropriate sharding approach can solve the problem.&lt;/p&gt;

&lt;p&gt;Of course, sharding isn't a silver bullet. Before choosing a sharding approach, ensure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You've optimized single-table query performance&lt;/li&gt;
&lt;li&gt;You've used appropriate indexes&lt;/li&gt;
&lt;li&gt;You've enabled WAL mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after these optimizations are complete and performance bottlenecks still exist should you consider introducing sharding. Being able to do simple things well is itself a capability.&lt;/p&gt;

&lt;p&gt;Many things are better done once than said once—now let the engineering results speak for themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;HagiCode Project Repository: &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;github.com/HagiCode-org/site&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SQLite WAL Mode Documentation: &lt;a href="https://www.sqlite.org/wal.html" rel="noopener noreferrer"&gt;sqlite.org/wal.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Orleans Distributed Framework: &lt;a href="https://dotnet.github.io/orleans/" rel="noopener noreferrer"&gt;dotnet.github.io/orleans&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Original Article &amp;amp; License
&lt;/h2&gt;

&lt;p&gt;Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.&lt;br&gt;
This article was created with AI assistance and reviewed by the author before publication.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author: &lt;a href="https://www.newbe.pro" rel="noopener noreferrer"&gt;newbe36524&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Original URL: &lt;a href="https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-04-17-sqlite-sharding-strategies-comparison%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-04-17-sqlite-sharding-strategies-comparison%2F&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sqlite</category>
      <category>hagicode</category>
    </item>
    <item>
      <title>How to Implement Automated Steam Publishing with GitHub Actions</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Thu, 16 Apr 2026 01:49:25 +0000</pubDate>
      <link>https://dev.to/newbe36524/how-to-implement-automated-steam-publishing-with-github-actions-3i7f</link>
      <guid>https://dev.to/newbe36524/how-to-implement-automated-steam-publishing-with-github-actions-3i7f</guid>
      <description>&lt;h1&gt;
  
  
  How to Implement Automated Steam Publishing with GitHub Actions
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;This article shares a complete solution for implementing automated Steam publishing in the HagiCode Desktop project, covering the full automation pipeline from GitHub Release to the Steam platform, including key technical details such as Steam Guard authentication and multi-platform Depot uploads.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;The Steam platform's publishing process is actually quite different from traditional application distribution methods. Steam has its own complete update distribution system. Developers need to upload build artifacts to Steam's CDN network using the SteamCMD tool, rather than just throwing out a download link like other platforms.&lt;/p&gt;

&lt;p&gt;The HagiCode Desktop project plans to launch on the Steam platform, which has brought some new challenges to our publishing process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Need to convert existing build artifacts into Steam-compatible format&lt;/li&gt;
&lt;li&gt;Must upload to Steam platform via SteamCMD tool&lt;/li&gt;
&lt;li&gt;Must handle Steam Guard authentication&lt;/li&gt;
&lt;li&gt;Need to support multi-platform (Linux, Windows, macOS) Depot uploads&lt;/li&gt;
&lt;li&gt;Need to implement automated flow from GitHub Release to Steam&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The project had previously implemented a "portable version mode" that allows the application to detect fixed service payloads packaged in the extra directory. Our goal is to seamlessly integrate this portable version mode with Steam distribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  About HagiCode
&lt;/h2&gt;

&lt;p&gt;The solution shared in this article comes from our practical experience in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode is an AI code assistant project that supports desktop execution. We are working on launching on the Steam platform, which is why we needed to establish a reliable automated publishing process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Design
&lt;/h2&gt;

&lt;p&gt;The core of the entire Steam publishing process is a GitHub Actions workflow that divides the process into three main stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│ GitHub Actions Workflow (Steam Release)                      │
├─────────────────────────────────────────────────────────────┤
│ 1. Preparation Phase:                                        │
│    - Checkout portable-version code                         │
│    - Download build artifacts from GitHub Release           │
│    - Extract and prepare Steam content directory            │
│                                                             │
│ 2. SteamCMD Setup:                                          │
│    - Install/reuse SteamCMD                                 │
│    - Authenticate using Steam Guard                         │
│                                                             │
│ 3. Publishing Phase:                                        │
│    - Generate Depot VDF configuration files                 │
│    - Generate App Build VDF configuration files             │
│    - Call SteamCMD to upload to Steam                       │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The advantages of this design are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reuses existing GitHub Release artifacts, avoiding duplicate builds&lt;/li&gt;
&lt;li&gt;Achieves security isolation through self-hosted runners&lt;/li&gt;
&lt;li&gt;Supports preview mode and formal release branch switching&lt;/li&gt;
&lt;li&gt;Complete error handling and logging&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Workflow Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Trigger Parameter Design
&lt;/h3&gt;

&lt;p&gt;Our workflow supports the following key parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;           &lt;span class="c1"&gt;# Portable Version release tag&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tag&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;publish&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(e.g.,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;v1.0.0)'&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;steam_preview&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="c1"&gt;# Whether to generate preview build&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Whether&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;enable&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;preview&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;mode'&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;
  &lt;span class="na"&gt;steam_branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;# Steam branch to set to live&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Target&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Steam&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;branch'&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;preview'&lt;/span&gt;
  &lt;span class="na"&gt;steam_description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Build description override&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;description'&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Self-Hosted Runner Configuration
&lt;/h3&gt;

&lt;p&gt;For security reasons, we use a self-hosted runner with the steam label:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;self-hosted&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Linux&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;X64&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;steam&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures that Steam publishing is executed on a dedicated runner, maintaining secure isolation of sensitive credentials.&lt;/p&gt;

&lt;h3&gt;
  
  
  Concurrency Control
&lt;/h3&gt;

&lt;p&gt;To prevent releases of the same version from interfering with each other, we configured concurrency control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portable-version-steam-${{ github.event.inputs.release }}&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;cancel-in-progress: false&lt;/code&gt; is set here because the Steam publishing process can be lengthy, and we don't want to cancel an ongoing release due to a new trigger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Script Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Preparing Release Input
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;prepare-steam-release-input.mjs&lt;/code&gt; script is responsible for preparing the input needed for publishing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Download build manifest and artifact inventory from GitHub Release&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buildManifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;downloadBuildManifest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;releaseTag&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;artifactInventory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;downloadArtifactInventory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;releaseTag&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Download compressed packages for each platform&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;linux-x64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;win-x64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;osx-universal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;artifactUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getArtifactUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;artifactInventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;downloadArtifact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;artifactUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Extract to Steam content directory structure&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;extractToSteamContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contentRoot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Steam Guard Authentication
&lt;/h3&gt;

&lt;p&gt;Steam requires using Steam Guard to protect accounts. We implemented a code generation algorithm based on shared secrets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateSteamGuardCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sharedSecret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decodeSharedSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sharedSecret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;timeBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeBigUInt64BE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="c1"&gt;// Use HMAC-SHA1 to generate time-based one-time code&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeBuffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Convert to 5-character Steam Guard code&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;steamGuardCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This implementation is based on Steam Guard's TOTP (Time-based One-Time Password) mechanism, generating a new verification code every 30 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  VDF Configuration Generation
&lt;/h3&gt;

&lt;p&gt;VDF (Valve Data Format) is the configuration format used by Steam. We need to generate two types of VDF files:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Depot VDF&lt;/strong&gt; is used to configure content for each platform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildDepotVdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;depotId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contentRoot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"DepotBuildConfig"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`  "DepotID" "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;escapeVdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;depotId&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`  "ContentRoot" "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;escapeVdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contentRoot&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;  "FileMapping"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;  {&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;    "LocalPath" "*"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;    "DepotPath" "."&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;    "recursive" "1"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;  }&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;App Build VDF&lt;/strong&gt; is used to configure the entire application build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildAppBuildVdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;depotBuilds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"appbuild"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`  "appid" "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`  "desc" "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;escapeVdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`  "contentroot" "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;escapeVdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contentRoot&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;  "buildoutput" "build_output"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;  "depots"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;  {&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;depotId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;depotVdfPath&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;depotBuilds&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;vdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`    "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;depotId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;depotVdfPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setLive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;vdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`  }`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;vdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`  "setlive" "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;setLive&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;vdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;vdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SteamCMD Invocation
&lt;/h3&gt;

&lt;p&gt;Finally, upload is performed by calling SteamCMD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;steamcmdPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;steamUsername&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;steamPassword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;steamGuardCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+run_app_build&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;appBuildPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+quit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This step is the final leap of the entire process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-Platform Depot Handling
&lt;/h2&gt;

&lt;p&gt;Steam uses the Depot system to manage content for different platforms. We support three main Depots:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Depot Identifier&lt;/th&gt;
&lt;th&gt;Architecture Support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Linux&lt;/td&gt;
&lt;td&gt;&lt;code&gt;linux-x64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;x64_64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;&lt;code&gt;win-x64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;x64_64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;osx-universal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;universal, x64_64, arm64&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each Depot has an independent content directory and VDF configuration file, ensuring that users on different platforms only download the content they need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing Process
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Prepare GitHub Release
&lt;/h3&gt;

&lt;p&gt;First, you need to create a GitHub Release in the portable-version repository, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compressed packages for each platform&lt;/li&gt;
&lt;li&gt;Build manifest (&lt;code&gt;{tag}.build-manifest.json&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Artifact inventory (&lt;code&gt;{tag}.artifact-inventory.json&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Trigger Steam Publishing Workflow
&lt;/h3&gt;

&lt;p&gt;Manually trigger the workflow through GitHub Actions and fill in the necessary parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;release&lt;/code&gt;: Version tag to publish (e.g., v1.0.0)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;steam_branch&lt;/code&gt;: Target branch (e.g., &lt;code&gt;preview&lt;/code&gt; or &lt;code&gt;public&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;steam_preview&lt;/code&gt;: Whether to enable preview mode&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Automatic Publishing Process
&lt;/h3&gt;

&lt;p&gt;The workflow will automatically execute the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download and extract GitHub Release artifacts&lt;/li&gt;
&lt;li&gt;Install/update SteamCMD&lt;/li&gt;
&lt;li&gt;Generate Steam VDF configuration files&lt;/li&gt;
&lt;li&gt;Authenticate using Steam Guard&lt;/li&gt;
&lt;li&gt;Upload content to Steam CDN&lt;/li&gt;
&lt;li&gt;Set specified branch to live&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Configuration Guide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Required Secrets Configuration
&lt;/h3&gt;

&lt;p&gt;Configure the following secrets in GitHub repository settings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret Name&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STEAM_USERNAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Steam account username&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STEAM_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Steam account password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STEAM_SHARED_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Steam Guard shared secret (optional)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STEAM_GUARD_CODE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Steam Guard code (optional)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STEAM_APP_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Steam application ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STEAM_DEPOT_ID_LINUX&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Linux Depot ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STEAM_DEPOT_ID_WINDOWS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Windows Depot ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STEAM_DEPOT_ID_MACOS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;macOS Depot ID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Environment Variable Configuration
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable Name&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Default Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PORTABLE_VERSION_STEAMCMD_ROOT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SteamCMD installation directory&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.local/share/portable-version/steamcmd&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Steam Guard Authentication Management
&lt;/h3&gt;

&lt;p&gt;First-time run requires manually entering the Steam Guard code. After that, it's recommended to configure a shared secret for automatic code generation. This avoids the need for manual intervention with each publish.&lt;/p&gt;

&lt;p&gt;SteamCMD will save the login token for subsequent reuse. However, note the token's validity period - it will need re-authentication after expiration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Content Directory Structure
&lt;/h3&gt;

&lt;p&gt;Ensure the Steam content directory structure is correct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;steam-content/
├── linux-x64/     # Linux platform content
├── win-x64/       # Windows platform content
└── osx-universal/ # macOS universal binary content
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each directory should contain the complete application files for the corresponding platform.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Preview Mode
&lt;/h3&gt;

&lt;p&gt;Preview mode does not set any branch to live, making it suitable for testing and verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;if [ "$STEAM_PREVIEW_INPUT" = 'true' ]; then&lt;/span&gt;
  &lt;span class="s"&gt;cmd+=(--preview)&lt;/span&gt;
&lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows uploading to the Steam platform for verification first, then switching to the formal branch after confirmation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error Handling and Logging
&lt;/h3&gt;

&lt;p&gt;The script includes comprehensive error handling and logging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify GitHub Release existence&lt;/li&gt;
&lt;li&gt;Check required metadata files&lt;/li&gt;
&lt;li&gt;Ensure platform content exists&lt;/li&gt;
&lt;li&gt;Generate GitHub Actions summary reports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This information is very valuable for debugging and auditing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Artifact Management
&lt;/h3&gt;

&lt;p&gt;The workflow generates two types of artifacts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;portable-steam-release-preparation-{tag}&lt;/code&gt;: Publishing preparation metadata&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;portable-steam-build-metadata-{tag}&lt;/code&gt;: Steam build metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These artifacts can be used for subsequent auditing and debugging. It's recommended to set the retention time to 30 days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Application
&lt;/h2&gt;

&lt;p&gt;In the HagiCode project, this automated publishing process has successfully run for multiple versions. The entire pipeline from GitHub Release to Steam platform is fully automated without manual intervention.&lt;/p&gt;

&lt;p&gt;This has significantly improved our publishing efficiency and reliability. Previously, manually publishing a version took over 30 minutes, but now the entire process can be completed in just a few minutes.&lt;/p&gt;

&lt;p&gt;More importantly, the automated process reduces the possibility of human error. Each publish follows a standardized process with more predictable results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Through the solution shared in this article, we have achieved:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Full automation from GitHub Release to Steam platform&lt;/li&gt;
&lt;li&gt;Support for multi-platform Depot uploads&lt;/li&gt;
&lt;li&gt;Security authentication based on Steam Guard&lt;/li&gt;
&lt;li&gt;Flexible switching between preview mode and formal publishing&lt;/li&gt;
&lt;li&gt;Comprehensive error handling and logging&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This solution is not only applicable to the HagiCode project but can also provide reference for other projects planning to launch on the Steam platform. If you're also considering Steam automated publishing, I hope the practices shared in this article can be helpful to you.&lt;/p&gt;

&lt;p&gt;If this article helps you, feel free to give a Star on HagiCode's GitHub repository or visit the official website for more information.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.valvesoftware.com/wiki/SteamCMD" rel="noopener noreferrer"&gt;SteamCMD Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://partner.steamgames.com/" rel="noopener noreferrer"&gt;Steamworks SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode Project Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode Official Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.hagicode.com/installation/docker-compose" rel="noopener noreferrer"&gt;HagiCode Installation Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hagicode.com/desktop/" rel="noopener noreferrer"&gt;HagiCode Desktop&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Original Article &amp;amp; License
&lt;/h2&gt;

&lt;p&gt;Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.&lt;br&gt;
This article was created with AI assistance and reviewed by the author before publication.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author: &lt;a href="https://www.newbe.pro" rel="noopener noreferrer"&gt;newbe36524&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Original URL: &lt;a href="https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-04-16-steam-release-automation-github-actions%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-04-16-steam-release-automation-github-actions%2F&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>githubactions</category>
      <category>steam</category>
      <category>devops</category>
      <category>cicd</category>
    </item>
  </channel>
</rss>
