<?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.us-east-2.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>How We Use impeccable to Optimize Frontend Interface Design and Implementation Stability</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Sat, 04 Jul 2026 13:29:21 +0000</pubDate>
      <link>https://dev.to/newbe36524/how-we-use-impeccable-to-optimize-frontend-interface-design-and-implementation-stability-41ol</link>
      <guid>https://dev.to/newbe36524/how-we-use-impeccable-to-optimize-frontend-interface-design-and-implementation-stability-41ol</guid>
      <description>&lt;h1&gt;
  
  
  How We Use impeccable to Optimize Frontend Interface Design and Implementation Stability
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Many teams face the same pain point when building UI: design language depends on experience, interaction details depend on memory, and the final result is often “usable but unstable.”&lt;/p&gt;

&lt;p&gt;In the HagiCode monorepo, &lt;code&gt;impeccable&lt;/code&gt; is not used as an “inspiration tool,” but embedded into an executable, verifiable, and regression-friendly engineering workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structured constraints at task entry (commands, scenarios, repository scope)&lt;/li&gt;
&lt;li&gt;Automatic alignment between site content and upstream command definitions&lt;/li&gt;
&lt;li&gt;Fixed local development ports, runtime parameters, and validation process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Based on the current implementation in the repository, this article breaks things into four parts: Background, Analysis, Solution, and Practice.&lt;/p&gt;

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

&lt;p&gt;In the current repository snapshot, the main lines directly related to &lt;code&gt;impeccable&lt;/code&gt; are concentrated in three places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Static site runtime orchestration&lt;/strong&gt;: &lt;code&gt;scripts/static-sites-dev.mjs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;impeccable documentation site&lt;/strong&gt;: &lt;code&gt;repos/impeccable-site&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI task preset contract example&lt;/strong&gt;: &lt;code&gt;designs/task-preset-plugin-examples/ui-master&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The third item is a design example, not the production backend runtime code itself. This distinction should be clarified first to avoid mistaking a “design contract” for “published behavior.”&lt;/p&gt;

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

&lt;h3&gt;
  
  
  1) Structure design intent first to reduce “freestyle” variance
&lt;/h3&gt;

&lt;p&gt;In the backend contract example of &lt;code&gt;ui-master&lt;/code&gt;, dependencies and inputs are locked first before execution begins.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;designs/task-preset-plugin-examples/ui-master/backend/task-preset.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;"taskKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uiMaster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scriptKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"autotask.ui-master"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requirements"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"skills"&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;"impeccable"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cli"&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;"impeccable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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;"exec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--offline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"impeccable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--help"&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="nl"&gt;"inputBindings"&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="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uiMasterDescription"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"promptParameter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uiMasterDescription"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"required"&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="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;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uiMasterCommandIds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"promptParameter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uiMasterCommandIds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"required"&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="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;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sceneKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"promptParameter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sceneKey"&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;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validate &lt;code&gt;skills:impeccable&lt;/code&gt; and &lt;code&gt;cli:impeccable&lt;/code&gt; first to avoid discovering broken links during execution.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;uiMasterCommandIds&lt;/code&gt; enforces command-based input, preventing “long descriptions but unclear direction.”&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sceneKey&lt;/code&gt; standardizes scenario keys, avoiding each preset inventing its own input name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stability value of this structure is direct: failures happen earlier, are explainable, and inputs are replayable.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Unify command catalog and documentation routes to reduce semantic drift
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;designs/task-preset-plugin-examples/ui-master/frontend/commands.json&lt;/code&gt; defines command grouping, documentation route templates, and command prelude:&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;"docs"&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;"baseUrl"&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://impeccable.hagicode.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"routeTemplate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/{lang}/docs/{commandId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"languageResolver"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"supported-language"&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;"commands"&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"craft"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"groupId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"skill"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"impeccable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"docsSlug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"craft"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"preludeTemplate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/impeccable {commandId} {uiMasterDescription}"&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 allows “execution commands, documentation descriptions, and language routing” to share the same set of IDs, reducing common mismatches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Called A in docs, called B in the panel&lt;/li&gt;
&lt;li&gt;Chinese page links to the wrong command in English&lt;/li&gt;
&lt;li&gt;Prompt prelude and docs slug are inconsistent&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3) Site implementation turns “content correctness” into a verifiable asset
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;repos/impeccable-site/package.json&lt;/code&gt;, &lt;code&gt;validate&lt;/code&gt; chains together the full quality gate:&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;"scripts"&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;"validate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run i18n:check &amp;amp;&amp;amp; npm run catalog:check &amp;amp;&amp;amp; npm run typecheck &amp;amp;&amp;amp; npm run test &amp;amp;&amp;amp; npm run build"&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;And the script behind &lt;code&gt;catalog:check&lt;/code&gt; (&lt;code&gt;scripts/build-command-catalog.mjs&lt;/code&gt;) performs hard validation on frontmatter structure:&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;parseFrontmatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sourceText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sourcePath&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;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sourceText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^---&lt;/span&gt;&lt;span class="se"&gt;\n([\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;?)\n&lt;/span&gt;&lt;span class="sr"&gt;---&lt;/span&gt;&lt;span class="se"&gt;\n?([\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;$/u&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&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;sourcePath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; must include YAML frontmatter`&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;frontmatter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&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;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isPlainObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frontmatter&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;sourcePath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; frontmatter must be a mapping`&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;frontmatter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of assert is simple, but crucial for stability: it is far better to fail locally and in CI than to discover missing document fields after deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) Fix development ports and env to avoid “works on my machine”
&lt;/h3&gt;

&lt;p&gt;In the monorepo root script &lt;code&gt;scripts/static-sites-dev.mjs&lt;/code&gt;, &lt;code&gt;impeccable&lt;/code&gt; is explicitly orchestrated:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;impeccable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Impeccable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;repoPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;repos/impeccable-site&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;productionUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://impeccable.hagicode.com/&lt;/span&gt;&lt;span class="dl"&gt;'&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="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="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;run&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;dev&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;PORT_IMPECCABLE_SITE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;36296&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;portHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;36296&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;56&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test &lt;code&gt;scripts/static-sites-dev.test.mjs&lt;/code&gt; also asserts these values (port, args, env, productionUrl).&lt;/p&gt;

&lt;p&gt;This “hardcoded + tested” approach is not flashy, but stable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unified local entry point&lt;/li&gt;
&lt;li&gt;Fixed troubleshooting path&lt;/li&gt;
&lt;li&gt;No drifting documentation site links&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;If you want to use &lt;code&gt;impeccable&lt;/code&gt; to optimize both &lt;strong&gt;interface quality&lt;/strong&gt; and &lt;strong&gt;implementation stability&lt;/strong&gt;, the existing repository practices can be implemented as four layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Contractualize task entry
&lt;/h3&gt;

&lt;p&gt;Approach: reuse preset structures like &lt;code&gt;ui-master&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Goals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restrict allowed repository scope (&lt;code&gt;targetRepositories&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Restrict command source (&lt;code&gt;uiMasterCommandIds&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Restrict context entry (&lt;code&gt;uiMasterDescription&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Benefit: every task assignment can be reviewed in terms of “what was the input, and why was it changed this way.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Standardize command semantics
&lt;/h3&gt;

&lt;p&gt;Approach: bind the three-piece set of command ID, docs slug, and prelude template.&lt;/p&gt;

&lt;p&gt;Goals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Commands selected in the panel can jump to docs with the same ID&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/impeccable &amp;lt;command&amp;gt;&lt;/code&gt; in prompts matches documentation semantics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Benefit: reduces hidden risk where “terminology is consistent but behavior is not.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: Validate content assets
&lt;/h3&gt;

&lt;p&gt;Approach: keep &lt;code&gt;npm run validate&lt;/code&gt; as a pre-commit gate.&lt;/p&gt;

&lt;p&gt;Goals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No missing i18n keys&lt;/li&gt;
&lt;li&gt;No drift between command catalog and upstream structure&lt;/li&gt;
&lt;li&gt;Type checks, tests, and build pass in one go&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Benefit: moves “occasional content issues” forward to local development.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 4: Standardize development runtime environment
&lt;/h3&gt;

&lt;p&gt;Approach: use &lt;code&gt;static-sites-dev&lt;/code&gt; for unified startup and fix &lt;code&gt;PORT_IMPECCABLE_SITE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Goals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimize port conflicts in team collaboration&lt;/li&gt;
&lt;li&gt;Fix multi-site joint debugging paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Benefit: less time spent on environment setup, faster issue localization to code itself.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Step 1: Initialize impeccable-site
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /home/newbe36524/repos/hagicode-mono/repos/impeccable-site
git submodule update &lt;span class="nt"&gt;--init&lt;/span&gt; &lt;span class="nt"&gt;--recursive&lt;/span&gt;
&lt;span class="nb"&gt;env&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; NPM_CONFIG_PREFIX npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Local development and full validation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;env&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; NPM_CONFIG_PREFIX npm run dev
&lt;span class="nb"&gt;env&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; NPM_CONFIG_PREFIX npm run validate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;validate&lt;/code&gt; covers i18n, catalog, typecheck, test, and build in one run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Run within unified monorepo orchestration
&lt;/h3&gt;

&lt;p&gt;From the monorepo root, run via the &lt;code&gt;scripts/static-sites-dev.mjs&lt;/code&gt; workflow and reuse the predefined &lt;code&gt;PORT_IMPECCABLE_SITE=36296&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The key point here is not just “it runs,” but “everyone runs it the same way.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Convert UI requests into command-based input
&lt;/h3&gt;

&lt;p&gt;In the task preset flow, prioritize landing requirements as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;uiMasterDescription&lt;/code&gt;: business objective&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;uiMasterCommandIds&lt;/code&gt;: commands for this run (e.g., &lt;code&gt;craft&lt;/code&gt;, &lt;code&gt;audit&lt;/code&gt;, &lt;code&gt;polish&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sceneKey&lt;/code&gt;: scenario context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then proceed to implementation changes to reduce direction drift caused by directly editing UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Clarify current evidence gaps
&lt;/h3&gt;

&lt;p&gt;Based on the current repository snapshot, what can be confirmed is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The site and validation workflow of &lt;code&gt;impeccable-site&lt;/code&gt; are implemented code.&lt;/li&gt;
&lt;li&gt;The deep integration contract between &lt;code&gt;ui-master&lt;/code&gt; and &lt;code&gt;impeccable&lt;/code&gt; is mainly presented in &lt;code&gt;designs/task-preset-plugin-examples&lt;/code&gt; and design documents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you plan to conclude that this is “fully enabled in production backend,” current evidence is insufficient, so that claim should not be made yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  HagiCode Information Overview
&lt;/h2&gt;

&lt;p&gt;HagiCode is a platform project that integrates multi-repository engineering, AI task execution, documentation sites, and toolchains. Its core capability is not a single-point “can generate code,” but chaining task input constraints, execution context, validation gates, and cross-repository collaboration into a stable delivery pipeline. Typical use cases include structured UI iteration, cross-repository feature integration testing, standards-driven technical content production, and automated delivery with quality gates.&lt;/p&gt;

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

&lt;p&gt;The real value of &lt;code&gt;impeccable&lt;/code&gt; is not “helping you think of a prettier button,” but “turning interface optimization into an engineering discipline.” In HagiCode’s current practice, this is achieved through four actions: task contractualization, unified command semantics, upfront content validation, and standardized runtime environment. Build these foundations first, then pursue more advanced design enhancements—the payoff is greater and more sustainable.&lt;/p&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-07-04-goal-preset-agent-compatible-prompt-variants%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-07-04-goal-preset-agent-compatible-prompt-variants%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>impeccable</category>
      <category>frontend</category>
      <category>stability</category>
      <category>hagicode</category>
    </item>
    <item>
      <title>Teaching Every Agent to Speak `/goal`: A Compatible Extension Design for HagiCode's Continuous-Work Preset</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Fri, 03 Jul 2026 15:12:26 +0000</pubDate>
      <link>https://dev.to/newbe36524/teaching-every-agent-to-speak-goal-a-compatible-extension-design-for-hagicodes-continuous-work-1flb</link>
      <guid>https://dev.to/newbe36524/teaching-every-agent-to-speak-goal-a-compatible-extension-design-for-hagicodes-continuous-work-1flb</guid>
      <description>&lt;h1&gt;
  
  
  Teaching Every Agent to Speak &lt;code&gt;/goal&lt;/code&gt;: A Compatible Extension Design for HagiCode's Continuous-Work Preset
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When you hand an AI Agent a "continuous work" task, the ideal experience looks like this: you write down a goal, pick the repositories it's allowed to touch, click submit, and the Agent drives straight toward that goal—no mid-course questions, no going off-topic, no reaching outside the allowed repositories.&lt;/p&gt;

&lt;p&gt;In HagiCode, this capability has a dedicated entry point—the &lt;code&gt;goal&lt;/code&gt; preset task, whose command surface is &lt;code&gt;/goal&lt;/code&gt;. The catch: &lt;strong&gt;not every Agent understands &lt;code&gt;/goal&lt;/code&gt;.&lt;/strong&gt; Claude and Codex have native continuous-work command semantics, so handing them &lt;code&gt;/goal&lt;/code&gt; just works; but other CLIs (Gemini, Copilot, iFlow, OpenCode, etc.) have no such slash command at all. Stuffing &lt;code&gt;/goal ...&lt;/code&gt; into them verbatim, at best, gets treated as an invalid command, and at worst gets executed literally and blows up.&lt;/p&gt;

&lt;p&gt;The laziest option at first was: just let &lt;code&gt;goal&lt;/code&gt; run on Claude / Codex only and reject every other Agent outright. But that shuts most of the Agent matrix out the door. This article is about how we made this actually work—&lt;strong&gt;using "Agent-aware" prompt variants, so natively supported Agents take the native command while unsupported Agents take a fallback prompt, keeping behavior aligned and the experience consistent.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The content is based on the &lt;code&gt;goal&lt;/code&gt; preset package under &lt;code&gt;repos/hagitask&lt;/code&gt; and the backend parsing implementation in &lt;code&gt;repos/hagicode-core&lt;/code&gt;.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  What &lt;code&gt;goal&lt;/code&gt; is
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;goal&lt;/code&gt; is a built-in task preset plugin package located at &lt;code&gt;repos/hagitask/presets/goal/&lt;/code&gt;. Its &lt;code&gt;manifest.json&lt;/code&gt; declares the plugin identity, icon, localization bundle, and frontend/backend assets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&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;"taskPresetId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"goal"&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.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"icon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"flag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"custom-executor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"experimental"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entrypoints"&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;"menuSurface"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"session-create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"drawerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"goal"&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;"ui"&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;"panel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./frontend/panel.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"commands"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./frontend/commands.json"&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;"backend"&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;"taskPreset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./backend/task-preset.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./backend/prompts.json"&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;Its purpose is "continuous work mode": the user fills in a goal description (&lt;code&gt;goalDescription&lt;/code&gt;), optionally scopes the writable / read-only repositories, and then the backend assembles it into a single non-interactive auto-task run. The key fields in the frontend panel &lt;code&gt;panel.json&lt;/code&gt; are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;goalCommandIds&lt;/code&gt;: the command selector, which by default contains only a single &lt;code&gt;/goal&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;goalDescription&lt;/code&gt;: the required goal description;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;targetRepositories&lt;/code&gt;: the repository access selector, distinguishing read / write.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where &lt;code&gt;/goal&lt;/code&gt; comes from
&lt;/h3&gt;

&lt;p&gt;The command surface is defined in &lt;code&gt;frontend/commands.json&lt;/code&gt;, and the core is a single line of &lt;code&gt;preludeTemplate&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&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;"commands"&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"goal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"goal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"preludeTemplate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/goal {goalDescription}"&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 other words, the final prompt sent to the Agent will have a &lt;code&gt;/goal &amp;lt;goal description&amp;gt;&lt;/code&gt; line prepended before the body. For Claude / Codex, this line is a command they recognize; for other Agents, this line is something that needs to be "translated." The adaptation challenge of this entire article is exactly this: how the meaning of that single &lt;code&gt;/goal ...&lt;/code&gt; line diverges across different Agents.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  A single-locale template no longer holds up
&lt;/h3&gt;

&lt;p&gt;HagiTask's early prompt packages were split &lt;strong&gt;by language only&lt;/strong&gt;: in &lt;code&gt;prompts.json&lt;/code&gt;, each locale declares one set of &lt;code&gt;systemTemplate&lt;/code&gt; + &lt;code&gt;userTemplate&lt;/code&gt;, and the backend &lt;code&gt;PresetTaskCatalogProvider&lt;/code&gt; reads them into &lt;code&gt;PromptPackage.Locales&lt;/code&gt;, then picks and renders one set at runtime based on the request language.&lt;/p&gt;

&lt;p&gt;This model is enough for presets whose "execution instructions are Agent-agnostic." But &lt;code&gt;goal&lt;/code&gt; isn't like that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude / Codex: can rely directly on the native &lt;code&gt;/goal&lt;/code&gt; command surface;&lt;/li&gt;
&lt;li&gt;Other Agents: need a more verbose prompt that explicitly teaches them "how to enter continuous work mode," and must also tell them not to execute &lt;code&gt;/goal ...&lt;/code&gt; as a command.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When two starkly different sets of instructions have to appear under the same locale, splitting by language alone is no longer enough. We needed to introduce another dimension beyond language: &lt;strong&gt;the Agent family&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Refusing to run isn't a good option
&lt;/h3&gt;

&lt;p&gt;Initially, &lt;code&gt;goal&lt;/code&gt;'s &lt;code&gt;requirements&lt;/code&gt; locked Agents down to &lt;code&gt;["claude", "codex"]&lt;/code&gt;, and any other Agent was immediately judged incompatible. This has two product downsides:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the hero selection panel, the compatible heroes get filtered down to very few, easily leaving "no hero to pick";&lt;/li&gt;
&lt;li&gt;Plenty of Agents are perfectly capable of doing continuous work—they just don't have a native &lt;code&gt;/goal&lt;/code&gt;—and a blanket rejection wastes their capability.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the real problem to solve isn't "who can run," but "&lt;strong&gt;for the same goal, how do we give different but equivalent execution instructions based on Agent capability.&lt;/strong&gt;" The answer: push the Agent-aware differences down into the prompt layer, solving it with variants + fallbacks, rather than blocking people at the admission layer.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  The variant selector: agent × language
&lt;/h3&gt;

&lt;p&gt;The new prompt package contract introduces &lt;code&gt;variants&lt;/code&gt; in &lt;code&gt;prompts.json&lt;/code&gt;, using &lt;code&gt;selectors&lt;/code&gt; to declare the matching dimensions and &lt;code&gt;entries&lt;/code&gt; to declare each variant's matching rules and template files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&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;"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.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"templateEngine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"handlebars"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"variants"&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;"selectors"&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;"agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"entries"&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"native-zh-cn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"match"&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;"agent"&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;"claude"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"codex"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"language"&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;"zh-CN"&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;"systemTemplate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./templates/zh-CN/native.system.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"userTemplate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./templates/zh-CN/native.user.hbs"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fallback-zh-cn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"match"&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;"language"&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;"zh-CN"&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;"systemTemplate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./templates/zh-CN/fallback.system.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"userTemplate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./templates/zh-CN/fallback.user.hbs"&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="c1"&gt;// ... native-en-us / fallback-en-us follow the same pattern&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;"fallback"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fallback-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;"systemTemplate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./templates/en-US/fallback.system.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"userTemplate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./templates/en-US/fallback.user.hbs"&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;Three levels of fallback, loosening progressively:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;native variant&lt;/strong&gt;: &lt;code&gt;match&lt;/code&gt; constrains both &lt;code&gt;agent&lt;/code&gt; (claude/codex) and &lt;code&gt;language&lt;/code&gt; at the same time, so only native Agents hit it;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fallback variant&lt;/strong&gt;: &lt;code&gt;match&lt;/code&gt; &lt;strong&gt;constrains only &lt;code&gt;language&lt;/code&gt;, not &lt;code&gt;agent&lt;/code&gt;&lt;/strong&gt;, so any Agent can hit it—this is the fallback for Agents that "have no native &lt;code&gt;/goal&lt;/code&gt;";&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fallback-default&lt;/strong&gt;: the final safety net for when even the language can't be matched.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Order matters: &lt;code&gt;entries&lt;/code&gt; is traversed in declaration order, with native placed ahead of fallback. So claude/codex hit native first; other Agents fall through to the fallback language variant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two sets of templates: native vs. fallback
&lt;/h3&gt;

&lt;p&gt;For the same goal, the difference between the two system prompts lies entirely in their attitude toward that &lt;code&gt;/goal&lt;/code&gt; line.&lt;/p&gt;

&lt;p&gt;The native template (&lt;code&gt;templates/zh-CN/native.system.md&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are the built-in execution prompt of the `goal` task preset plugin.

This run must use `/goal` as the command surface for continuous work mode. Treat the provided project paths and repository scope as authoritative work boundaries.

This run is in a non-interactive environment. Do not question the user; when reasonable assumptions allow you to keep making progress, proceed directly and state those assumptions explicitly in your response.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fallback template (&lt;code&gt;templates/zh-CN/fallback.system.md&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are the fallback execution prompt of the `goal` task preset plugin.

The current agent may not support the native `/goal` command. If a `/goal ...` line appears at the front of the prompt, treat it as goal metadata attached by the preset—do not treat it as a slash command that must be executed verbatim.

Please complete the task in continuous work mode, and treat the provided project paths and repository scope as authoritative work boundaries.

This run is in a non-interactive environment. Do not question the user; when reasonable assumptions allow you to keep making progress, proceed directly and state those assumptions explicitly in your response.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That core sentence is the soul of the fallback strategy: &lt;strong&gt;"If a &lt;code&gt;/goal ...&lt;/code&gt; line appears at the front of the prompt, treat it as goal metadata attached by the preset—do not treat it as a slash command that must be executed verbatim."&lt;/strong&gt; The &lt;code&gt;/goal &amp;lt;goal&amp;gt;&lt;/code&gt; line that &lt;code&gt;preludeTemplate&lt;/code&gt; splices in is a command in the eyes of a native Agent, but for a fallback Agent it is explicitly downgraded to "metadata / a goal marker," so that ordinary prompt following reproduces the same continuous-work behavior.&lt;/p&gt;

&lt;p&gt;The user templates (&lt;code&gt;.hbs&lt;/code&gt;) also come in two sets, and the fallback set has an extra "routing note" section that explicitly tells the Agent it was routed to a branch that doesn't rely on native &lt;code&gt;/goal&lt;/code&gt;, and requires it to "not rely on native &lt;code&gt;/goal&lt;/code&gt; support; instead use ordinary prompt-following to accomplish the same continuous-work behavior."&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend: one resolution decides it all
&lt;/h3&gt;

&lt;p&gt;Variant matching happens in the backend at &lt;code&gt;ResolvePromptSelection&lt;/code&gt; in &lt;code&gt;PCode.Application/Services/PresetTaskCatalogProvider.cs&lt;/code&gt;. It first resolves the locale, then packs language and agent into a selector dictionary, and finds the first matching variant in order:&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="n"&gt;PresetTaskResolvedPromptSelection&lt;/span&gt; &lt;span class="nf"&gt;ResolvePromptSelection&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;agentFamily&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;resolvedLocale&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ResolvePromptLocale&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;PromptPackage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UsesVariants&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Legacy presets split by locale only take the legacy path, preserving backward compatibility&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;legacyTemplate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PromptPackage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Locales&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;resolvedLocale&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="nf"&gt;PresetTaskResolvedPromptSelection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;resolvedLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;NormalizeAgentFamily&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agentFamily&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;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"legacy-locales"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;legacyTemplate&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;normalizedAgentFamily&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;NormalizeAgentFamily&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agentFamily&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;selectorInputs&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="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&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;PresetTaskPromptSelectorNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Language&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resolvedLocale&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;normalizedAgentFamily&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;selectorInputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PresetTaskPromptSelectorNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;normalizedAgentFamily&lt;/span&gt;&lt;span class="p"&gt;!;&lt;/span&gt;
    &lt;span class="p"&gt;}&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;variant&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PromptPackage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Variants&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;VariantMatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selectorInputs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&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="nf"&gt;PresetTaskResolvedPromptSelection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;resolvedLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normalizedAgentFamily&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variant&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="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"variants"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Template&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// No variant hit: fall to the explicitly declared fallback variant&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PresetTaskResolvedPromptSelection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;resolvedLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normalizedAgentFamily&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PromptPackage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FallbackVariant&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="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"variants"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PromptPackage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FallbackVariant&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Template&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;PromptPackage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Locales&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;resolvedLocale&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 matching rule &lt;code&gt;VariantMatches&lt;/code&gt; is dead simple—&lt;strong&gt;whatever selectors a variant declares, they all have to match at runtime&lt;/strong&gt;; undeclared dimensions impose no constraint:&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;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;VariantMatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;PresetTaskPromptVariantDefinition&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IReadOnlyDictionary&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;selectorInputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&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;selector&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Match&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;selectorInputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;runtimeValue&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;false&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;selector&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;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runtimeValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&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;false&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;true&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 explains why a fallback variant that only writes &lt;code&gt;language&lt;/code&gt; can cover all Agents: it doesn't constrain the &lt;code&gt;agent&lt;/code&gt; dimension at all, so any Agent whose language matches will hit it. The native variant, by contrast, adds one more constraint on &lt;code&gt;agent&lt;/code&gt;, so only claude/codex can satisfy it.&lt;/p&gt;

&lt;p&gt;Where does the Agent family come from? At runtime it's mapped from the selected hero's executor type—see &lt;code&gt;PresetTaskRequirementModels.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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;executorType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;executorType&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;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClaudeCodeCli&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Claude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CodexCli&lt;/span&gt;      &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Codex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GitHubCopilot&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Copilot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IFlowCli&lt;/span&gt;      &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iflow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OpenCodeCli&lt;/span&gt;   &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Opencode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ... gemini / kimi / qoder / kiro / hermes / codebuddy / pi ...&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;null&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 creating a session, &lt;code&gt;SessionsController.PresetTasks.cs&lt;/code&gt; first gets the hero, then calls &lt;code&gt;PresetTaskAgentFamilies.Resolve(hero.ExecutorType)&lt;/code&gt; to get the family name and feeds it to &lt;code&gt;ResolvePromptSelection&lt;/code&gt;. After a variant is selected, &lt;code&gt;RenderPrompt&lt;/code&gt; renders the template with Handlebars and splices the &lt;code&gt;/goal &amp;lt;goal&amp;gt;&lt;/code&gt; generated by &lt;code&gt;preludeTemplate&lt;/code&gt; right at the front:&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;renderedPrompt&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="nf"&gt;FillTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalizedContext&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;commandPrelude&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;BuildCommandPrelude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalizedContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// -&amp;gt; "/goal &amp;lt;goalDescription&amp;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;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commandPrelude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;renderedPrompt&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;commandPrelude&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;\n\n&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;renderedPrompt&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Loosening admission: from an allowlist to &lt;code&gt;any&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;With a fallback prompt in place, &lt;code&gt;goal&lt;/code&gt; no longer needs to shut non-native Agents out. The &lt;code&gt;requirements&lt;/code&gt; in &lt;code&gt;backend/task-preset.json&lt;/code&gt; loosens from &lt;code&gt;["claude","codex"]&lt;/code&gt; to &lt;code&gt;any&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&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;"taskKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"goal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requirements"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"agent"&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;"any"&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;"inputBindings"&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="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"goalCommandIds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"promptParameter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"goalCommandIds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"required"&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="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;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"goalDescription"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"promptParameter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"goalDescription"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"required"&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="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;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"targetScopeMarkdown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"promptParameter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"targetScopeMarkdown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"producer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"frontend-computed"&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;Open up the admission layer, push the differences down to the prompt layer—this is the key trade-off of the whole design: &lt;strong&gt;compatibility is no longer a switch of "can it run," but a routing of "which prompt set does it run with."&lt;/strong&gt;&lt;/p&gt;

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

&lt;h3&gt;
  
  
  What an end-to-end run looks like
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;code&gt;goal&lt;/code&gt; drawer, the user fills in the goal description, selects the repository scope, picks a hero, and submits;&lt;/li&gt;
&lt;li&gt;The backend resolves the agent family (e.g. &lt;code&gt;claude&lt;/code&gt; / &lt;code&gt;gemini&lt;/code&gt;) from the selected hero's executor type;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ResolvePromptSelection(locale, agentFamily)&lt;/code&gt; finds a variant by &lt;code&gt;agent × language&lt;/code&gt;:

&lt;ul&gt;
&lt;li&gt;claude/codex + zh-CN → &lt;code&gt;native-zh-cn&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;gemini + zh-CN → native doesn't hit, falls to &lt;code&gt;fallback-zh-cn&lt;/code&gt;;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RenderPrompt&lt;/code&gt; renders the selected template and splices &lt;code&gt;/goal &amp;lt;goal description&amp;gt;&lt;/code&gt; at the front;&lt;/li&gt;
&lt;li&gt;A native Agent executes the first line as a command; a fallback Agent, following the system prompt, treats the first line as metadata and reproduces continuous work via ordinary prompt following.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Adding Agent awareness to your own preset
&lt;/h3&gt;

&lt;p&gt;If you want to do the same adaptation for a new preset, the routine is fixed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In &lt;code&gt;prompts.json&lt;/code&gt;, replace &lt;code&gt;locales&lt;/code&gt; with &lt;code&gt;variants&lt;/code&gt;, and add &lt;code&gt;agent&lt;/code&gt; to &lt;code&gt;selectors&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;The native variant's &lt;code&gt;match&lt;/code&gt; writes both &lt;code&gt;agent&lt;/code&gt; and &lt;code&gt;language&lt;/code&gt;; the fallback variant writes only &lt;code&gt;language&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Then declare a top-level &lt;code&gt;variants.fallback&lt;/code&gt; as the final safety net (the schema requires that the variants mode must have a fallback);&lt;/li&gt;
&lt;li&gt;The backend needs no changes—&lt;code&gt;ResolvePromptSelection&lt;/code&gt; / &lt;code&gt;VariantMatches&lt;/code&gt; are generic;&lt;/li&gt;
&lt;li&gt;If admission shouldn't be restricted, set the &lt;code&gt;requirements&lt;/code&gt; agent to &lt;code&gt;any&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A few easy pitfalls
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Variant order is priority.&lt;/strong&gt; native must come before fallback, otherwise the fallback that only constrains language would grab all Agents first, and claude/codex would never reach the native branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fallback prompt must explicitly "downgrade" that &lt;code&gt;/goal&lt;/code&gt; line.&lt;/strong&gt; The &lt;code&gt;/goal ...&lt;/code&gt; prefix is spliced on unconditionally (decided by &lt;code&gt;preludeTemplate&lt;/code&gt;), so the sentence in the fallback system prompt—"treat it as metadata, don't execute it verbatim"—cannot be omitted, or a non-native Agent might treat it as an invalid command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The non-interactive constraint must go into every template.&lt;/strong&gt; &lt;code&gt;goal&lt;/code&gt; is a queued auto-task, and the templates repeatedly emphasize "don't use AskUserQuestion, don't question, proceed with reasonable assumptions and state them," precisely to keep the Agent from getting stuck waiting for input while unattended.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repository boundaries are enforced by the prompt, not by a sandbox.&lt;/strong&gt; The templates explicitly require "only modify within repositories explicitly selected as writable, and treat read-only repositories as supplementary context." This is a soft constraint at the prompt layer, so fill in the repository scope carefully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backward compatibility is a free lunch.&lt;/strong&gt; When an old preset writes no &lt;code&gt;variants&lt;/code&gt; and only writes &lt;code&gt;locales&lt;/code&gt;, &lt;code&gt;UsesVariants&lt;/code&gt; is false, and it goes straight down the &lt;code&gt;legacy-locales&lt;/code&gt; path—completely unaffected—so you don't have to touch existing presets just for the new model.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;"If an Agent doesn't natively support &lt;code&gt;goal&lt;/code&gt;, how do we extend it to make it work"—the answer isn't to hard-code a pile of branches for each Agent, but to model the difference as &lt;code&gt;agent × language&lt;/code&gt; prompt variants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Natively supported Agents (Claude / Codex) take the native variant and use the &lt;code&gt;/goal&lt;/code&gt; command surface directly;&lt;/li&gt;
&lt;li&gt;Unsupported Agents take the fallback variant, explicitly downgrade &lt;code&gt;/goal ...&lt;/code&gt; to goal metadata, and reproduce the same continuous-work behavior via ordinary prompt following;&lt;/li&gt;
&lt;li&gt;Matching relies on the generic rule that "whatever selectors a variant declares must all match" + progressive fallbacks, resolved once and for all on the backend;&lt;/li&gt;
&lt;li&gt;Admission loosens from an allowlist to &lt;code&gt;any&lt;/code&gt;, so compatibility shifts from "can it run" to "which prompt set does it run with."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The value of this design isn't limited to the single &lt;code&gt;goal&lt;/code&gt; preset: the &lt;code&gt;agent × language&lt;/code&gt; selector + fallback model is generic, reusable by any preset that needs to "give different instructions based on Agent capability," and with zero disruption to old presets.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Preset package: &lt;code&gt;repos/hagitask/presets/goal/&lt;/code&gt; (&lt;code&gt;manifest.json&lt;/code&gt;, &lt;code&gt;backend/task-preset.json&lt;/code&gt;, &lt;code&gt;backend/prompts.json&lt;/code&gt;, &lt;code&gt;backend/templates/{en-US,zh-CN}/{native,fallback}.*&lt;/code&gt;, &lt;code&gt;frontend/commands.json&lt;/code&gt;, &lt;code&gt;frontend/panel.json&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Prompt package schema: &lt;code&gt;repos/hagitask/schemas/task-preset-plugin/prompt-package.schema.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Backend variant resolution: &lt;code&gt;repos/hagicode-core/src/PCode.Application/Services/PresetTaskCatalogProvider.cs&lt;/code&gt; (&lt;code&gt;ResolvePromptSelection&lt;/code&gt; / &lt;code&gt;VariantMatches&lt;/code&gt; / &lt;code&gt;RenderPrompt&lt;/code&gt; / &lt;code&gt;BuildCommandPrelude&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Agent family mapping: &lt;code&gt;repos/hagicode-core/src/PCode.Application/Services/PresetTaskRequirementModels.cs&lt;/code&gt; (&lt;code&gt;PresetTaskAgentFamilies.Resolve&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Session creation: &lt;code&gt;repos/hagicode-core/src/PCode.HttpApi/Controllers/SessionsController.PresetTasks.cs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;OpenSpec capability and proposals: &lt;code&gt;openspec/specs/goal-preset-task/spec.md&lt;/code&gt;, &lt;code&gt;openspec/changes/archive/2026-06-23-preset-task-agent-requirement-support/&lt;/code&gt;, &lt;code&gt;openspec/changes/archive/2026-06-26-support-agent-language-prompt-selection-in-hagitask/&lt;/code&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-07-03-goal-preset-agent-compatible-prompt-variants%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-07-03-goal-preset-agent-compatible-prompt-variants%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>agents</category>
      <category>hagicode</category>
    </item>
    <item>
      <title>How to Use last30days for AI Research Based on Social Networks</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Sat, 27 Jun 2026 10:33:43 +0000</pubDate>
      <link>https://dev.to/newbe36524/how-to-use-last30days-for-ai-research-based-on-social-networks-59o1</link>
      <guid>https://dev.to/newbe36524/how-to-use-last30days-for-ai-research-based-on-social-networks-59o1</guid>
      <description>&lt;h1&gt;
  
  
  How to Use last30days for AI Research Based on Social Networks
&lt;/h1&gt;

&lt;p&gt;Every day on social networks, people express themselves: posting, voting, arguing, unsubscribing, migrating. These scattered voices on Reddit, X, YouTube comments, TikTok, Hacker News, and Polymarket are just noise when viewed individually, but together they form a fairly authentic slice of public opinion. The only problem is—no one wants to manually browse through thirty days of posts across eight platforms just to reach a research conclusion.&lt;/p&gt;

&lt;p&gt;That's what the last30days skill was built for. It pulls together the last thirty days of content from these channels for you and synthesizes it by topic. The preset task layer then turns "calling last30days" from a command that requires memorizing parameters into a ready-to-run form.&lt;/p&gt;

&lt;p&gt;This article isn't about how useful last30days itself is. Rather, it's about how HagiCode uses the preset task orchestration mechanism to solidly deliver "one skill, four research postures." After all, a skill is a capability, and a preset is the glue that turns capabilities into products.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background: What Preset Really Saves Is That Sentence Said Over and Over
&lt;/h2&gt;

&lt;p&gt;The preset task system originally bound a skill at the preset level. That is, one preset corresponded to one skill, with a relatively single way of calling it. But in reality, the same capability often needs to be used in different postures—with last30days, sometimes you want to do general research, sometimes you want competitive comparison, and sometimes you only want to analyze prompt style for a specific product.&lt;/p&gt;

&lt;p&gt;Hard-coding these different usage patterns into one interaction flow makes the interface increasingly messy; splitting them into four independent skills duplicates work. So the design took a middle step: &lt;strong&gt;allow binding skills at the command level&lt;/strong&gt;. One preset hangs multiple commands, each command self-describing which skill it calls and with what parameters; the preset-level requirements then serve as an authoritative list declaring which skills this preset depends on as a whole.&lt;/p&gt;

&lt;p&gt;This way, interface stays interface, execution stays execution. Who wants to mix interface styles with execution logic anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Skill, Four Modes
&lt;/h2&gt;

&lt;p&gt;The last30days preset is the exemplar of this paradigm: &lt;strong&gt;one skill, four commands&lt;/strong&gt;. Four commands correspond to four research postures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;general (broad research)&lt;/strong&gt;: Give a query and let last30days pull relevant discussions from each platform over the last thirty days and synthesize a conclusion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;comparison&lt;/strong&gt;: Explicitly specify the objects to compare, letting the skill organize material around "who's better/who's worse/their respective pain points."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;competitors&lt;/strong&gt;: Focus on a product's competitive ecosystem, calling last30days's built-in &lt;code&gt;--competitors&lt;/code&gt; flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;prompting (prompt style)&lt;/strong&gt;: Focus on how people actually use it and ask questions, oriented toward usage and mental models.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four commands declare &lt;code&gt;"skill": "last30days"&lt;/code&gt; in &lt;code&gt;commands.json&lt;/code&gt;, but each has a different prelude (parameters and prompts). In the frontend drawer, users select a mode, write a query, and check target repositories; as for which command gets assembled behind the scenes, the preset decides that itself. Users don't need to memorize parameters or know about flags like &lt;code&gt;--competitors&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Data Layers: commands.json and task-preset.json
&lt;/h2&gt;

&lt;p&gt;Two files in the preset package share responsibilities quite cleanly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;commands.json&lt;/code&gt;&lt;/strong&gt;: Describes "what commands exist and what each command looks like." Each command comes with a &lt;code&gt;skill&lt;/code&gt; field explaining which skill it calls, plus its own prelude template explaining how to assemble this command into a single-line instruction. This is command-level self-description.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;task-preset.json&lt;/code&gt;&lt;/strong&gt;: Describes "what this preset needs overall." It holds requirements (the skill dependency list), inputs definitions, inputBindings, and metadata like selectionMode. This is the preset-level authoritative list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The boundaries between the two layers are quite clear: commands are responsible for saying "I need this skill," and the preset is responsible for saying "which skills my preset allows." If a command declares a skill but it's not in the preset's requirements, validation fails with diagnostic &lt;code&gt;command-skill-not-in-requirements&lt;/code&gt;. This explicit failure is much better than silent degradation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Single-Line Injection: &lt;code&gt;/{skill} {prelude}&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;What actually connects the skill into the execution flow is an unassuming little mechanism: single-line injection.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PresetTaskCatalogProvider&lt;/code&gt; has a method called &lt;code&gt;CombineCommandSkillPrelude&lt;/code&gt;. The logic is simple: if a command declares a &lt;code&gt;skill&lt;/code&gt; and its rendered prelude line doesn't start with &lt;code&gt;/{skill}&lt;/code&gt;, prefix &lt;code&gt;/{skill}&lt;/code&gt; onto it. What finally gets handed to the executor is a standalone instruction of the form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/last30days {commandPrelude}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then comes the body rendered by &lt;code&gt;user.hbs&lt;/code&gt;: mode shaping, query, target repository boundaries, and the constraint of "non-interactive, record assumptions."&lt;/p&gt;

&lt;p&gt;Why do this? Because skills like last30days themselves load and run upon "receiving a &lt;code&gt;/{skill}&lt;/code&gt; instruction." The preset can't assume the skill will be automatically triggered; it must explicitly feed this instruction in. Just one line, but it's the key that makes the whole chain work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five-Stage Technical Chain
&lt;/h2&gt;

&lt;p&gt;Putting all this together, from user clicking submit to conclusion linking back to the repository, there are five stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Frontend selection&lt;/strong&gt;: Users select mode, write query, and check target repositories in &lt;code&gt;CreatePresetTaskDrawer&lt;/code&gt;. &lt;code&gt;resolveCommandPreview&lt;/code&gt; mirrors the backend assembly logic to show the preview instruction in real time; &lt;code&gt;buildTargetScopeMarkdown&lt;/code&gt; groups by read/write and calculates repository boundaries as a markdown segment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend validation&lt;/strong&gt;: The request lands in &lt;code&gt;SessionsController.PresetTasks.TryResolvePresetTaskRequestAsync&lt;/code&gt;. It first validates that inputs/targets/derived keys are on the whitelist, command id is in the catalog, selectionMode is single, then runs &lt;code&gt;PresetTaskRequirementCheckService.CheckAsync&lt;/code&gt;: for each skill in requirements, it checks the local skill inventory via &lt;code&gt;LocalSkillCommandAdapter&lt;/code&gt; and deduplicates by CacheKey.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt rendering&lt;/strong&gt;: Renders the corresponding &lt;code&gt;user.hbs&lt;/code&gt; according to locale, injecting &lt;code&gt;last30daysMode&lt;/code&gt;, &lt;code&gt;last30daysQuery&lt;/code&gt;, &lt;code&gt;targetScopeMarkdown&lt;/code&gt;, and &lt;code&gt;targetRepositories&lt;/code&gt;. &lt;code&gt;user.hbs&lt;/code&gt; explicitly forbids &lt;code&gt;AskUserQuestion&lt;/code&gt;, requiring the executor to record assumptions rather than ask questions when encountering ambiguity—this is the hard boundary of non-interactive execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skill loading and execution&lt;/strong&gt;: The executor receives the assembled prompt. &lt;code&gt;/last30days&lt;/code&gt; as a standalone instruction comes first, triggering skill loading. Inside the skill, it runs according to its own LAWs: Step 0.45 does keyword-trap pre-check (preventing query from being mistaken for handle/subreddit), Step 0.5/0.55 parses directed channels, Step 0.75 lets the reasoning model generate &lt;code&gt;--plan&lt;/code&gt; itself, then runs the Python engine to pull data from Reddit/X/YouTube/TikTok/Instagram/HN/Polymarket/Web, Step 2 supplements with WebSearch, Step 2.5 appends to the raw file, and finally synthesizes a conclusion according to LAWs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conclusion linking back&lt;/strong&gt;: The synthesized conclusion is brought back into the preset context. The read/write boundaries of target repositories rely on the soft constraint of &lt;code&gt;targetScopeMarkdown&lt;/code&gt; in the prompt—the preset tells the skill "you can only read these and write those," and the skill follows this boundary during its own execution.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Several Practical Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non-interactive boundaries must be written into the template&lt;/strong&gt;. Disabling &lt;code&gt;AskUserQuestion&lt;/code&gt; in &lt;code&gt;user.hbs&lt;/code&gt; isn't a suggestion, it's a hard constraint. When a preset runs, no one sits in front of the screen answering back-questions, so ambiguities must be digested by "recording assumptions."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-set flags that can be preset&lt;/strong&gt;. The competitors mode writes &lt;code&gt;--competitors&lt;/code&gt; directly into the prelude; users don't need to know this flag exists. The meaning of a preset is to hide professional parameters in the right hands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skill deduplication relies on CacheKey&lt;/strong&gt;. It doesn't matter if the same skill appears multiple times in requirements; &lt;code&gt;PresetTaskRequirementCheckService&lt;/code&gt; deduplicates by CacheKey, so it won't repeatedly check or load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repository boundaries are soft constraints&lt;/strong&gt;. Read/write boundaries rely on prompt injection, not sandboxing. This means whether the skill "obeys" depends partly on how well it follows the prompt. This is a realistic compromise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failures should be explicit, not silent&lt;/strong&gt;. Missing skills should report requirement check failure, return 400, and provide a "one-click install" entry in the frontend (&lt;code&gt;openSkillGalleryForSkill&lt;/code&gt;). Letting users know what's broken and how to fix it is far more responsible than quietly degrading into "can run without skill."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration should be non-destructive&lt;/strong&gt;. The evolution from preset-level skill to per-command skill is backward compatible. Old preset-level declarations remain valid; new command-level declarations are additive capabilities, not replacements. Taking too big a step is easy to tear something, so why bother.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Preset task turns one skill into four product forms, not relying on any sophisticated algorithm, but on several contracts with cleanly drawn boundaries: command self-description, preset authoritative list, single-line injection, non-interactive hard constraints, explicit failure. Putting these together, "using last30days for social network research" moves from a command requiring memorized parameters to a feature that runs by filling out a form.&lt;/p&gt;

&lt;p&gt;As for what these voices actually prove when gathered together—that's last30days's own job...&lt;/p&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-last30days-social-ai-research%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-02-11-last30days-social-ai-research%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>last30days</category>
      <category>hagicode</category>
      <category>skill</category>
    </item>
    <item>
      <title>Precise Routing for Every Command: Practical Implementation of Multi-Skill Support in HagiCode Preset Task</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Tue, 23 Jun 2026 11:52:41 +0000</pubDate>
      <link>https://dev.to/newbe36524/precise-routing-for-every-command-practical-implementation-of-multi-skill-support-in-hagicode-13na</link>
      <guid>https://dev.to/newbe36524/precise-routing-for-every-command-practical-implementation-of-multi-skill-support-in-hagicode-13na</guid>
      <description>&lt;h1&gt;
  
  
  Precise Routing for Every Command: Practical Implementation of Multi-Skill Support in HagiCode Preset Task
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;A preset packed with multiple commands, yet they all have to share the same skill requirements? This refactor lets each command independently declare its dependent skill, and visualizes this binding on the panel—badges, summaries, one-click installation, all in one smooth flow.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Let me start with some context.&lt;/p&gt;

&lt;p&gt;HagiCode's preset task is a plugin-based mini-tool system. Users don't need to manually type commands; just fill in a few fields on the visual panel and click to create an automated task session. Each preset is essentially a directory that typically looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;manifest.json&lt;/code&gt;: preset identity information&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;panel.json&lt;/code&gt;: visual panel form definition&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;commands.json&lt;/code&gt;: list of commands to actually execute&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;task-preset.json&lt;/code&gt; or &lt;code&gt;prompts.json&lt;/code&gt;: task parameters and skill requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This system is indeed convenient to use, but we quickly ran into an awkward limitation.&lt;/p&gt;

&lt;p&gt;In the early versions, skills could only be declared in the &lt;code&gt;requirements&lt;/code&gt; array at the preset level. What does this mean? It means all commands within the same preset share the same skill requirements. It might not sound like a big deal, but in practice, you encounter scenarios like this:&lt;/p&gt;

&lt;p&gt;A preset has five commands, where the first one wants to use the &lt;code&gt;last30days&lt;/code&gt; skill, the third one wants to use &lt;code&gt;ui-master&lt;/code&gt;, and the remaining three don't need any skills. This was impossible in the old design. If you wanted different commands to route to different skills, you had to forcibly split these commands into multiple presets, and configuration would bloat immediately.&lt;/p&gt;

&lt;p&gt;This is exactly the problem that proposal &lt;code&gt;extend-preset-task-multiple-skills-support&lt;/code&gt; aims to solve: let each command independently declare the skill it depends on, and visualize this binding in the UI.&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 the preset task system is precisely its quick action entry point for users. Every change discussed below was refined through actual pitfalls and optimizations—after all, "reading books is no substitute for practicing." The project source code is at &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode-org/site&lt;/a&gt;, interested readers can go give it a Star first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clarifying the Problem First: Why Not a Mapping Table?
&lt;/h2&gt;

&lt;p&gt;Before diving in, the most intuitive solution is: create another &lt;code&gt;commandSkillMappings&lt;/code&gt; mapping table to store the "command ID → skill" relationship separately. This sounds clean, separation of concerns and all that.&lt;/p&gt;

&lt;p&gt;But on closer reflection, it doesn't hold up.&lt;/p&gt;

&lt;p&gt;Each command in &lt;code&gt;commands.json&lt;/code&gt; already has an ID, and this ID would need to be copied into the mapping table. Two files, same ID—as soon as someone changes a command and forgets to sync the mapping table, the data drifts apart. This "separation for separation's sake" design has maintenance costs far outweighing the little cleanliness it brings. In the end, it just adds trouble.&lt;/p&gt;

&lt;p&gt;So we ultimately chose a more direct path: &lt;strong&gt;put an optional &lt;code&gt;skill&lt;/code&gt; field directly on the command definition&lt;/strong&gt;. A command declares which skill it binds to itself, maintained locally, and nothing gets disconnected.&lt;/p&gt;

&lt;p&gt;Behind this decision, there's a more important design principle worth pulling out separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core One: Two-Layer Data Responsibility Separation
&lt;/h2&gt;

&lt;p&gt;This is the most critical insight in the entire refactor.&lt;/p&gt;

&lt;p&gt;Many people's first reaction is: since commands now have &lt;code&gt;skill&lt;/code&gt;, shouldn't we scan each command's &lt;code&gt;skill&lt;/code&gt; field when doing requirement check (skill gatekeeper inspection)?&lt;/p&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;We deliberately split this into two layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;skill&lt;/code&gt; field in &lt;code&gt;commands.json&lt;/code&gt;&lt;/strong&gt;: only responsible for declaring binding. It tells the system "this command binds to which skill," used for rendering prompt preambles and UI display.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;requirements&lt;/code&gt; array in &lt;code&gt;task-preset.json&lt;/code&gt;&lt;/strong&gt;: this is the authoritative enumeration. It's the real gatekeeper, determining which skills a preset needs to satisfy to run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, &lt;code&gt;skill&lt;/code&gt; answers "which to bind, what to render," while &lt;code&gt;requirements&lt;/code&gt; answers "whether it's actually allowed to run." These are two separate things, don't mix them together.&lt;/p&gt;

&lt;p&gt;The benefit of this split is that the check logic is naturally simple. Because the gatekeeper is always based on preset-level &lt;code&gt;requirements&lt;/code&gt;, deduplicated by &lt;code&gt;CacheKey&lt;/code&gt;, multiple commands binding the same skill only get probed once, without repeated hits. Command-level skills don't introduce any additional probing overhead.&lt;/p&gt;

&lt;p&gt;This principle is also the fundamental reason we rejected the mapping table approach—a mapping table would mislead people into thinking "binding equals gatekeeping," stirring the two layers of responsibilities back together. Outsmarting yourself, plain and simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Two: What Does a Command Definition Look Like?
&lt;/h2&gt;

&lt;p&gt;The refactored command definition simply adds an optional &lt;code&gt;skill&lt;/code&gt; field to the original structure. Taking the &lt;code&gt;last30days&lt;/code&gt; bundled preset as an example, its &lt;code&gt;commands.json&lt;/code&gt; roughly looks like this:&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;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"../../schemas/commands.schema.json"&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.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"commands"&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"research"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"skill"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"last30days"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"调研一下最近30天大家对 {topic} 的真实讨论"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"summarize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;Several points to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;version&lt;/code&gt; upgraded to &lt;code&gt;1.1&lt;/code&gt;, and the corresponding schema also added the optional &lt;code&gt;skill&lt;/code&gt; field.&lt;/li&gt;
&lt;li&gt;The first command &lt;code&gt;research&lt;/code&gt; binds to the &lt;code&gt;last30days&lt;/code&gt; skill, and execution will route to this skill.&lt;/li&gt;
&lt;li&gt;The second command &lt;code&gt;summarize&lt;/code&gt; doesn't bind to any skill, it's just a regular instruction taking the default path.&lt;/li&gt;
&lt;li&gt;Note here that &lt;strong&gt;no&lt;/strong&gt; requirement is written in the command. The real gatekeeper is in the &lt;code&gt;requirements&lt;/code&gt; of &lt;code&gt;task-preset.json&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&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;"requirements"&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="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"last30days"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cacheKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"skill:last30days"&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 &lt;code&gt;last30days&lt;/code&gt; bound by the &lt;code&gt;research&lt;/code&gt; command must appear in this &lt;code&gt;requirements&lt;/code&gt;, otherwise there's a problem—which is exactly the hard constraint discussed in the next section. You can't force things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Three: Cross-Validation During Loading
&lt;/h2&gt;

&lt;p&gt;Declaring bindings in data alone isn't enough; someone needs to provide a safety net to prevent "orphan bindings" where a command binds to a skill but it's never declared in requirements from making it to production.&lt;/p&gt;

&lt;p&gt;This safety net is &lt;code&gt;ValidateCommandSkills&lt;/code&gt;. It runs once during preset package loading, checking each command's &lt;code&gt;skill&lt;/code&gt; one by one to see if it can find a corresponding entry in the preset-level &lt;code&gt;requirements&lt;/code&gt;. If not found, it judges the package as illegal, directly disables the entire preset, and throws diagnostic code &lt;code&gt;command-skill-not-in-requirements&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why disable the entire package instead of just skipping that command? Because a preset is a whole, and commands often have dependencies (one command's output feeds into the next). If you silently skip one, the following commands get empty input, and behavior becomes completely unpredictable. After all, you can't read people's minds, and you can't read code's either. Better to let users see explicit errors than to let tasks mysteriously go off the rails halfway. This point can't be sloppy.&lt;/p&gt;

&lt;p&gt;This validation is completed during the loading phase, meaning the problem is discovered the moment the preset is registered, not dragged out until the user actually clicks "run" to explode. For user experience, early errors are always better than late errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Four: Idempotent Splicing of Prompt Preambles
&lt;/h2&gt;

&lt;p&gt;Next is the most subtle link in the execution chain.&lt;/p&gt;

&lt;p&gt;When a command binds to a skill, such as &lt;code&gt;last30days&lt;/code&gt;, the system needs to "splice" this skill information in front of the command before actual execution, forming a complete single-line instruction to pass to the executor. This process is handled by &lt;code&gt;CombineCommandSkillPrelude&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let me give a concrete example. The &lt;code&gt;research&lt;/code&gt; command's prompt is "调研一下最近30天大家对 {topic} 的真实讨论", and the bound skill is &lt;code&gt;last30days&lt;/code&gt;, so the final instruction passed to the executor roughly becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/last30days 调研一下最近30天大家对 {topic} 的真实讨论
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is, the &lt;code&gt;/last30days&lt;/code&gt; preamble is added before the prompt. When the executor sees this preamble, it knows to first switch context to the &lt;code&gt;last30days&lt;/code&gt; skill.&lt;/p&gt;

&lt;p&gt;There's a pitfall here: idempotency.&lt;/p&gt;

&lt;p&gt;Why emphasize idempotency? Because in some scenarios, the prompt itself might already carry this skill preamble (for example, the user manually wrote half, or copied it from somewhere else). If the system foolishly splices it again, it becomes &lt;code&gt;/last30days /last30days 调研...&lt;/code&gt;, and the executor either errors or behaves abnormally.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;CombineCommandSkillPrelude&lt;/code&gt; detects before splicing; if the prefix already exists, it doesn't add it again. This step seems insignificant, but it can block a class of very subtle bugs.&lt;/p&gt;

&lt;p&gt;It's worth mentioning that this entire preamble injection logic is completed at the preset definition layer ( &lt;code&gt;BuildCommandPrelude&lt;/code&gt; in &lt;code&gt;PresetTaskCatalogProvider&lt;/code&gt; ), and the session creation code on the &lt;code&gt;SessionsController&lt;/code&gt; side doesn't need to change at all. This is also the benefit brought by separation of concerns—the execution entry remains stable, and the complexity of skill routing is contained within the definition layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Five: How the Frontend Displays Bindings
&lt;/h2&gt;

&lt;p&gt;With the backend straightening out the data model and execution chain, the final step is to let users "see" this binding in the interface. After all, if users can't perceive a feature, it's essentially not done.&lt;/p&gt;

&lt;p&gt;The frontend did three things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, add badges to the command selector.&lt;/strong&gt; In the command-picker, a small badge appears next to each command bound to a skill, indicating which skill it depends on. Users can tell at a glance which command is "skill-enabled" and which is a regular command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, requirement-check summary block.&lt;/strong&gt; The panel has a dedicated summary area listing all skill requirements the current preset needs to satisfy, and which skill each command binds to. The data for this block comes from the &lt;code&gt;commandSkillsByRequirementKey&lt;/code&gt; mapping—grouping and aggregating commands by their bound requirement key, so users can easily compare whether "requirements" match "actual bindings". Trying to draw a tiger and ending up with a dog—something like that—so the aggregation logic needs to be straightforward, not flashy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third, one-click installation deep link on failure.&lt;/strong&gt; If requirement check finds a skill isn't installed, users don't need to go dig through documentation to find the installation entry. The interface directly gives a deep link button; click to jump to the corresponding installation process. This step compresses the distance between "discovering a problem" and "solving a problem" to the shortest possible.&lt;/p&gt;

&lt;p&gt;The frontend types are also restrained, the command type just adds a &lt;code&gt;skill?: string&lt;/code&gt;, and normalization is done (&lt;code&gt;|| undefined&lt;/code&gt;), avoiding empty strings and other boundary values causing trouble in subsequent judgment logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice: Five Steps to Complete the Refactor
&lt;/h2&gt;

&lt;p&gt;Stringing together the scattered points from before, the entire refactor is actually just five steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Extend schema&lt;/strong&gt;: &lt;code&gt;commands.schema.json&lt;/code&gt; adds optional &lt;code&gt;skill&lt;/code&gt; field, version number upgraded to &lt;code&gt;1.1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parse + validate&lt;/strong&gt;: &lt;code&gt;NormalizeCommands&lt;/code&gt; is responsible for parsing command definitions, &lt;code&gt;ValidateCommandSkills&lt;/code&gt; does cross-validation, command skills must be findable in preset-level requirements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inject preamble&lt;/strong&gt;: &lt;code&gt;BuildCommandPrelude&lt;/code&gt; idempotently splices the &lt;code&gt;/skill&lt;/code&gt; preamble before commands before execution, no need to modify &lt;code&gt;SessionsController&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrate bundled preset&lt;/strong&gt;: &lt;code&gt;last30days&lt;/code&gt; and &lt;code&gt;ui-master&lt;/code&gt; built-in presets' &lt;code&gt;commands.json&lt;/code&gt; get modified, adding the &lt;code&gt;skill&lt;/code&gt; field to corresponding commands. Migration only touches commands.json, doesn't touch other files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend visualization&lt;/strong&gt;: types add field, command-picker adds badges, requirement-check adds summary block, deep link for one-click installation on failure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A few points to note in practice, listed separately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One command can only bind to one skill&lt;/strong&gt;. This is the current constraint. If a scenario really needs one command to trigger multiple skills, the escape hatch is to declare multiple skills in the preset-level &lt;code&gt;requirements&lt;/code&gt;, letting them coexist at the preset level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagnostic code for validation failure&lt;/strong&gt; is &lt;code&gt;command-skill-not-in-requirements&lt;/code&gt;, search this code directly when troubleshooting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend normalization&lt;/strong&gt; remember &lt;code&gt;|| undefined&lt;/code&gt;, don't let empty strings sneak into judgment logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When migrating, only touch commands.json&lt;/strong&gt;, keep requirements unchanged to avoid introducing unexpected changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend testing&lt;/strong&gt; covers three scenarios: command skill in requirements (pass), not in requirements (disable package), multiple commands binding same skill (deduplication normal).&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;This multi-skill support refactor for preset task,表面上 just adds a &lt;code&gt;skill&lt;/code&gt; field to commands, but it pulls out a design question worth pondering: &lt;strong&gt;should binding and gatekeeping be separated?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our answer is separate. The &lt;code&gt;skill&lt;/code&gt; field only manages "which to bind, what to render," while &lt;code&gt;requirements&lt;/code&gt; manages "whether it's allowed to run." Once these two layers of responsibilities are mixed together, whether using a mapping table or some other form, subsequent validation, deduplication, and UI display become awkward. After separating, each layer becomes simple: the gatekeeper is always based on one authoritative enumeration, bindings are maintained locally and won't drift, preamble splicing is idempotent and controllable, and the UI just displays already clear data.&lt;/p&gt;

&lt;p&gt;Looking back, the entire refactor didn't use any fancy technology, it just relied on cleanly cutting responsibilities and then having each layer provide the safety net it should. After this round of polishing, HagiCode's preset task system can finally route every command precisely to the skill it should go to. In the end, things should have been this simple all along...&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-org/site&lt;/a&gt;: project source code, complete implementation of preset task system is here.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode 官网&lt;/a&gt;: understand HagiCode's overall capabilities.&lt;/li&gt;
&lt;li&gt;OpenSpec proposal &lt;code&gt;extend-preset-task-multiple-skills-support&lt;/code&gt;: original design document for this refactor, containing proposal, design, and tasks.&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-06-23-hagicode-preset-task-multiple-skills-support%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-23-hagicode-preset-task-multiple-skills-support%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>hagicode</category>
      <category>skill</category>
      <category>ai</category>
    </item>
    <item>
      <title>How HagiCode Integrates 13 Agent CLI Tools into a Single System</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Mon, 22 Jun 2026 12:15:25 +0000</pubDate>
      <link>https://dev.to/newbe36524/how-hagicode-integrates-13-agent-cli-tools-into-a-single-system-3kh1</link>
      <guid>https://dev.to/newbe36524/how-hagicode-integrates-13-agent-cli-tools-into-a-single-system-3kh1</guid>
      <description>&lt;h1&gt;
  
  
  How HagiCode Integrates 13 Agent CLI Tools into a Single System
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Honestly, it's not that hard, yet not that simple either. Let's talk about how we use a layered architecture to uniformly manage diverse Agent CLIs like Claude Code, Codex, Copilot, and Gemini—and how we can plug in a new one at any time.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;The story started abruptly, born from a pretty headache-inducing problem.&lt;/p&gt;

&lt;p&gt;Agent CLIs have sprung up like bamboo shoots in recent years—Claude Code, OpenAI Codex, GitHub Copilot, Gemini CLI, Kimi, Qoder, Kiro... a new one pops up every few months. As a project that wants users to "install one HagiCode and use the full suite of Agents," we couldn't bet on just one CLI. But we also couldn't write a complete set of logic for each CLI covering installation, health checks, and scheduling—the code would bloat until it was unmaintainable, like a tangled ball of yarn that no one dares touch.&lt;/p&gt;

&lt;p&gt;More troublesome, these CLIs have wildly different temperaments: some use stdio, some gRPC, some only give you a shell entry point, and streaming output formats each speak their own language. If we wrote judgments like &lt;code&gt;if (provider == ClaudeCode)&lt;/code&gt; directly in business code, within six months it would become a lump of "legacy code" no one dares touch. After all, who wants to touch a brick that looks ready to crumble?&lt;/p&gt;

&lt;p&gt;To contain all these pain points, we made a decision: &lt;strong&gt;add a thin abstraction layer and shared runtime between the business layer and specific CLIs&lt;/strong&gt;. This looks simple, yet it directly determines whether HagiCode can quickly integrate new CLIs. I'll explain the specifics shortly.&lt;/p&gt;

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

&lt;p&gt;The solution shared here comes from our hands-on practice in the HagiCode project. HagiCode is an AI code assistant integration platform with a very pure goal—use one installation, one configuration, to bring in all mainstream Agent CLIs for users.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Number "13" Came About
&lt;/h2&gt;

&lt;p&gt;First, a number that gets asked repeatedly—why 13 Agent CLIs.&lt;/p&gt;

&lt;p&gt;Actually, the answer is hidden right there in the &lt;code&gt;AIProviderType&lt;/code&gt; enum, like bamboo shadows outside a window—as long as you're willing to look, you can see it. The original definition looks 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;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;AIProviderType&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ClaudeCodeCli&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;CodexCli&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="n"&gt;GitHubCopilot&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="n"&gt;CodebuddyCli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;OpenCodeCli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IFlowCli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// Deprecated&lt;/span&gt;
    &lt;span class="n"&gt;HermesCli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;QoderCli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;KiroCli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;KimiCli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;GeminiCli&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="n"&gt;DeepAgentsCli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ReasonixCli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PiCli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;13&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 enum has 14 values total, but the &lt;code&gt;IFlowCli=5&lt;/code&gt; path is now blocked. In &lt;code&gt;AIProviderFactory&lt;/code&gt;, it's explicitly shut out:&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;providerType&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IFlowCli&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="nf"&gt;NotSupportedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"IFlowCli is no longer supported"&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;Combined with filtering through &lt;code&gt;IsActivelySupportedProviderType()&lt;/code&gt;, the ones actually "alive" in the system are &lt;strong&gt;13&lt;/strong&gt;: Claude Code, Codex, GitHub Copilot, CodeBuddy, OpenCode, Hermes, Qoder, Kiro, Kimi, Gemini, DeepAgents, Reasonix, Pi.&lt;/p&gt;

&lt;p&gt;That's where "13" comes from. Not a marketing number—counted straight from the code. After all, numbers don't lie. Only we do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layered Architecture: Locking Down Change
&lt;/h2&gt;

&lt;p&gt;The core idea behind connecting 13 CLIs comes down to one sentence: &lt;strong&gt;let business code not care which specific CLI it's calling&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We broke it down into six layers, looking from top to bottom:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Identity Layer — &lt;code&gt;AIProviderType&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The enum is each CLI's "ID number." Anywhere a CLI is mentioned, this enum value identifies it. Strings and enums convert between each other via &lt;code&gt;ToStringValue()&lt;/code&gt; / &lt;code&gt;ToAIProviderType()&lt;/code&gt;. Simple, yet indispensable.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Business Contract Layer — &lt;code&gt;IAIProvider&lt;/code&gt; / &lt;code&gt;IAIProviderFactory&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The business side only recognizes the &lt;code&gt;IAIProvider&lt;/code&gt; interface, which defines universal actions like "send a prompt, get a streaming response." As for whether it's Claude or Codex underneath—business doesn't care. Like sending a letter: you hand it over, what the mailman's surname is, who cares?&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Adapter Layer — &lt;code&gt;*CliProvider&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Each CLI corresponds to a thin adapter, like &lt;code&gt;PiCliProvider&lt;/code&gt;, &lt;code&gt;ReasonixCliProvider&lt;/code&gt;, &lt;code&gt;ClaudeCodeCliProvider&lt;/code&gt;. These adapters have very little to do: translate universal business requests into parameters specific CLI can understand, then translate specific CLI output back. They're deliberately written thin—adding a new CLI basically means copying an existing one and tweaking.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Shared Runtime Layer — &lt;code&gt;ICliProvider&amp;lt;TOptions&amp;gt;&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This layer lives in &lt;code&gt;HagiCode.Libs&lt;/code&gt; and does the real dirty work: launching processes cross-platform, handling stdio transport, parsing streaming output, handling timeouts and retries. All adapters reuse the same runtime, so when onboarding a new CLI, process management basically doesn't need rewriting.&lt;/p&gt;

&lt;p&gt;To use an analogy: the adapter layer is the "interpreter," the shared runtime layer is the "delivery company." The interpreter just makes sure the message is clear; how the package gets delivered, whether there's traffic on the road—that's the delivery company's concern. Each does their job, and the world is peaceful.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Factory Routing Layer — &lt;code&gt;AIProviderFactory&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Inside &lt;code&gt;CreateProvider&lt;/code&gt;, a &lt;code&gt;switch&lt;/code&gt; instantiates the corresponding adapter based on &lt;code&gt;AIProviderType&lt;/code&gt;, validating &lt;code&gt;IsConfigured&lt;/code&gt; along the way. This is the only place that "knows concrete types," strictly isolated in the factory. Change is only allowed in one corner; everywhere else stays clean.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Directory / UI Projection Layer — &lt;code&gt;main-professions.yaml&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This layer is interesting—it's not code, it's data.&lt;/p&gt;

&lt;p&gt;The primary profession list (role profiles like "I'm a frontend dev," "I'm a backend dev," "I'm a full-stack dev") is driven by the preset file &lt;code&gt;main-professions.yaml&lt;/code&gt;, read through &lt;code&gt;HeroPrimaryProfessionPresetProvider&lt;/code&gt;, then projected to the frontend UI. Adding a new primary profession requires no code changes—just edit YAML. Data instead of code, peace of mind.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;By the way, this is the biggest refactor in HagiCode. Early versions had a code-embedded registry called &lt;code&gt;AgentCliInstallRegistry&lt;/code&gt;. Later we found maintenance cost too high—more code, more tired people—so the whole thing got torn down and replaced with a data-driven + health monitoring approach. This is also why HagiCode can now rapidly expand profession types.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How Installation Gets Solved
&lt;/h2&gt;

&lt;p&gt;13 CLIs all need installation, each with different official installation methods—that's another mountain.&lt;/p&gt;

&lt;p&gt;Our approach is &lt;strong&gt;Docker Compose pre-installation + external management fallback&lt;/strong&gt;. The Docker image pre-installs mainstream CLIs (Claude Code, Codex, Copilot, CodeBuddy, OpenCode, Qoder, Kiro, Kimi, Gemini, Pi), so users pull the image and use it—no typing commands one by one. Installed, naturally in a better mood.&lt;/p&gt;

&lt;p&gt;For those needing separate installation in local environments, the installation command matrix looks roughly like this (verified against official docs):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CLI&lt;/th&gt;
&lt;th&gt;Official Installation Method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install -g @anthropic-ai/claude-code&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install -g @openai/codex&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install -g @github/copilot&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CodeBuddy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install -g @tencent-ai/codebuddy-code&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm i -g opencode-ai@latest&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qoder&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install -g @qoder-ai/qodercli&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kiro&lt;/td&gt;
&lt;td&gt;`curl -fsSL &lt;a href="https://cli.kiro.dev/install" rel="noopener noreferrer"&gt;https://cli.kiro.dev/install&lt;/a&gt; \&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kimi&lt;/td&gt;
&lt;td&gt;{% raw %}`curl -LsSf &lt;a href="https://code.kimi.com/install.sh" rel="noopener noreferrer"&gt;https://code.kimi.com/install.sh&lt;/a&gt; \&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;npm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hermes&lt;/td&gt;
&lt;td&gt;Official script, docs-only fallback preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepAgents / Reasonix&lt;/td&gt;
&lt;td&gt;See respective official docs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The frontend {% raw %}&lt;code&gt;PrimaryProfessionCard.tsx&lt;/code&gt; also changed accordingly—it now &lt;strong&gt;has no "Install CLI" button&lt;/strong&gt;, but instead displays CLI availability, version detection results, and a fallback hint that "this CLI is externally managed." In other words, whether it installs successfully is the system layer's responsibility; UI only faithfully reports status. Writing state and logic separately, they'll eventually drift apart—so why bother?&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Takes to Add a New CLI
&lt;/h2&gt;

&lt;p&gt;In practice, adding a new CLI to HagiCode comes down to these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add an enum value to &lt;code&gt;AIProviderType&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Copy an existing &lt;code&gt;*CliProvider&lt;/code&gt;, modify it for the new CLI's parameters and output parsing&lt;/li&gt;
&lt;li&gt;Add a routing line in &lt;code&gt;AIProviderFactory&lt;/code&gt;'s &lt;code&gt;switch&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If entering the primary profession directory, configure it in &lt;code&gt;main-professions.yaml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add an installation command to the image (or fall back to external management)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Through this entire process, core changes don't exceed two hundred lines of code—that's the true value of this abstraction. Each additional CLI has very low marginal cost; business code doesn't need a single change. All roads lead to Rome, ours just happens to be a slightly easier walk.&lt;/p&gt;

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

&lt;p&gt;Looking back, "integrating 13 CLIs" sounds daunting, but broken down, it's really two layers of work:&lt;/p&gt;

&lt;p&gt;One layer is &lt;strong&gt;isolating change&lt;/strong&gt;—through &lt;code&gt;AIProviderType&lt;/code&gt; enum + &lt;code&gt;IAIProvider&lt;/code&gt; contract + thin adapters + shared runtime, decoupling business code from specific CLIs. The other layer is &lt;strong&gt;data-ifying configuration&lt;/strong&gt;—using YAML presets like &lt;code&gt;main-professions.yaml&lt;/code&gt; to drive the directory and UI, avoiding touching code every time something is added.&lt;/p&gt;

&lt;p&gt;This solution is what stabilized after we stumbled through pits and iterated a few rounds in actual HagiCode development. If you're working on a similar "multi-provider integration" system, I hope this layered approach gives you some reference. After all, Agent CLIs will keep popping up in the coming years—an architecture that can quickly integrate new CLIs matters far more than "how many are supported right now"...&lt;/p&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-06-22-hagicode-13-agent-cli-integration-architecture%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-22-hagicode-13-agent-cli-integration-architecture%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>hagicode</category>
      <category>agentcli</category>
      <category>ai</category>
    </item>
    <item>
      <title>Solving Backend Distributed Challenges in AI Programming Workbenches with Orleans</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Sun, 21 Jun 2026 07:30:48 +0000</pubDate>
      <link>https://dev.to/newbe36524/solving-backend-distributed-challenges-in-ai-programming-workbenches-with-orleans-2nc3</link>
      <guid>https://dev.to/newbe36524/solving-backend-distributed-challenges-in-ai-programming-workbenches-with-orleans-2nc3</guid>
      <description>&lt;h1&gt;
  
  
  Solving Backend Distributed Challenges in AI Programming Workbenches with Orleans
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Managing dozens of AI CLI tools in a single process while pulling real-time streams across dozens of sessions—sounds like a pipe dream? Honestly, we thought it was pretty ridiculous too. But Orleans's Virtual Actor model actually tames this complexity quite beautifully. You know how some tools are born to solve specific problems? You just don't realize how perfect they are until you actually encounter that problem.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Building an AI programming workbench product has a unique architectural quirk: each user session is essentially a living, stateful "lifeform" that can engage with you for an hour or two. When a user drops in a message, the system needs to pick an appropriate AI Provider—Claude Code, Codex, Gemini, Kimi, CodeBuddy, etc., just listing the names takes fingers and toes—then spin up subprocesses, push execution results back in real-time through streaming channels, and synchronize various state changes over SignalR.&lt;/p&gt;

&lt;p&gt;If you tried to tackle this with a traditional stateless HTTP + Redis approach, you'd face some headaches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-provider management is fragmented everywhere&lt;/strong&gt;. Each AI CLI tool has its own process model, streaming output format, and timeout temperament. Mixing a dozen different logics together, the code quickly becomes—you know—spaghetti. Not that it's inedible, it just gives you indigestion.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Timeouts are uncontrollable, it's pure luck&lt;/strong&gt;. An AI operation might finish in three minutes, or drag on for two hours. Use a global unified timeout config? Then short operations get pointlessly cut off—think about it, that's unfair to users. Conversely, long operations chewing up the thread pool isn't a pretty picture either.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Concurrency needs careful budgeting, since GPUs don't fall from the sky&lt;/strong&gt;. Run too many AI operations simultaneously and machine resources max out immediately; but being too conservative means wasting compute power you paid for—it's like cranking the AC to 16 degrees and then piling on quilts. You need precise control over active session counts based on global permissions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;State management is complex enough to make you question life choices&lt;/strong&gt;. Each session has its own message queue, phase state, bound executor—this is stateful data. Forcing it into a stateless HTTP model means using Redis as universal glue. It sticks, sure, then you discover you've written a mountain of serialization/deserialization and distributed lock logic. After finishing, you stare at the screen: am I solving business problems, or fighting infrastructure?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These problems together aren't so much a technical challenge as a soul-searching interrogation of your architecture choices.&lt;/p&gt;

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

&lt;p&gt;These ideas didn't materialize out of thin air. The solution shared in this article comes from our real-world battle scars in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode is a desktop workbench for AI-assisted collaborative programming. Its backend coordinates dozens of AI CLI tools in a single process while providing low-latency real-time responses to the frontend—basically, we want the horse to run, not eat grass, and sing while running.&lt;/p&gt;

&lt;p&gt;The Orleans architecture discussed below is exactly what we honed and optimized during HagiCode's development. If you find this solution interesting, it shows our engineering foundation isn't bad—so HagiCode itself might be worth a closer look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Selection: Why Orleans
&lt;/h2&gt;

&lt;p&gt;Facing that soul-searching interrogation above, we seriously considered three paths:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approach A: Stateless API + Redis state management.&lt;/strong&gt; The logic is simple enough—pull session state from Redis per request, execute, write back. Horizontal scaling feels comfortable, but the Redis state structure grows alongside the business until you're not sure if you're maintaining a cache or an implicit database. State consistency relies on locks, streaming communication requires additional WebSocket/SSE routing layers. Essentially, Redis is just a shared dictionary here—it can't provide the stateful abstraction you really need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approach B: Actor model frameworks (Dapr / Akka.NET).&lt;/strong&gt; Dapr's Actor capability is sufficient, but it requires deploying a Sidecar—for local desktop products, that's not just overkill, it's like driving a tank to buy groceries. Akka.NET's Actor model leans more toward low-latency short tasks. For long-lifecycle workflows lasting an hour or two, you have to handle persistence and recovery yourself—the framework doesn't provide a safety net.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approach C: Microsoft Orleans.&lt;/strong&gt; When we saw Orleans's Virtual Actor model—how to put it—it felt like searching everywhere for your keys, then realizing they're in your pocket. Several features seem literally tailor-stitched for our scenario:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automatic Activation/Deactivation management&lt;/strong&gt;: You don't worry about when grains are born or die—the runtime handles it all. One session corresponds to one grain, session alive means grain alive, session ends means grain auto-recycled. This "don't have to care" feeling—only people who've experienced manual lifecycle management understand.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Native streaming support with &lt;code&gt;IAsyncEnumerable&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/strong&gt;: From CLI process output to frontend display, fully async streaming throughout, no intermediate buffer queues needed. Just this one feature saved us at least a thousand lines of hand-written glue code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;[AlwaysInterleave]&lt;/code&gt; and &lt;code&gt;[ResponseTimeout]&lt;/code&gt;&lt;/strong&gt;: Fine-grained concurrency and timeout control, configured per interface level, not a global一刀切. Finally no more painful choices between "all short" or "all long."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Built-in persistent state (&lt;code&gt;IPersistentState&amp;lt;T&amp;gt;&lt;/code&gt;)&lt;/strong&gt;: State auto-persists, no need for extra distributed cache setup. Peace of mind, seriously.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In evaluation, Orleans almost perfectly checked all boxes for HagiCode backend's core needs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Orleans Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stateful sessions&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;IPersistentState&amp;lt;T&amp;gt;&lt;/code&gt; + SQLite Shard persistence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming output&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;IAsyncEnumerable&amp;lt;T&amp;gt;&lt;/code&gt; native support, auto-penetrates to SignalR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long timeout control&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;[ResponseTimeout("02:00:00")]&lt;/code&gt; configured per interface granularity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider polymorphic routing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ExecutorGrainFactory&lt;/code&gt; dispatches based on &lt;code&gt;AIProviderType&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrency control&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SessionConcurrencyManager&lt;/code&gt; with grain single-threaded scheduling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Five Core Design Decisions
&lt;/h2&gt;

&lt;p&gt;Choosing the right tool is just step one. How to implement it is where the real skill shows. Below are five key designs we distilled after stepping into pits, climbing out, and brushing ourselves off. Some are experience, some are lessons, some... forget it, just read them yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Facade Grain Pattern
&lt;/h3&gt;

&lt;p&gt;The system's core scheduling grain is &lt;code&gt;SessionGrain&lt;/code&gt;. But it doesn't directly handle all logic—if it did, it would become a god class with tens of thousands of lines. God classes—you feel omnipotent writing them, worthless modifying them.&lt;/p&gt;

&lt;p&gt;We delegate domain-specific logic to two runtime components: &lt;code&gt;ChatSessionGrain&lt;/code&gt; handles chat mode, &lt;code&gt;ProposalSessionGrain&lt;/code&gt; handles proposal mode.&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;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SessionGrain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SessionGrain&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IServiceProvider&lt;/span&gt; &lt;span class="n"&gt;serviceProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IExecutorGrainFactory&lt;/span&gt; &lt;span class="n"&gt;executorGrainFactory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IMessageService&lt;/span&gt; &lt;span class="n"&gt;messageService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;PersistentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"session"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;IPersistentState&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SessionState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&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;ISessionGrain&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="n"&gt;ChatSessionGrain&lt;/span&gt; &lt;span class="n"&gt;ChatSessionComponent&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;_chatSessionComponent&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;ChatSessionGrain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RuntimeContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="n"&gt;ProposalSessionGrain&lt;/span&gt; &lt;span class="n"&gt;ProposalSessionComponent&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;_proposalSessionComponent&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;ProposalSessionGrain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RuntimeContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="n"&gt;ISessionRuntimeComponent&lt;/span&gt; &lt;span class="nf"&gt;GetRuntimeComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SessionType&lt;/span&gt; &lt;span class="n"&gt;sessionType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;sessionType&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;SessionType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Chat&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ChatSessionComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;SessionType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Proposal&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ProposalSessionComponent&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentOutOfRangeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionType&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 pattern is clean: grain identity is stable, doesn't change with session type; external callers only deal with &lt;code&gt;ISessionGrain&lt;/code&gt;, they don't care how work is split internally; components themselves are stateless, can be rebuilt on demand; both share the same &lt;code&gt;SessionState&lt;/code&gt; persistence, data consistency naturally handled. Who says architectural design can't be elegant?&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Polymorphic Executor Factory
&lt;/h3&gt;

&lt;p&gt;HagiCode supports a dozen AI CLI tools, each requiring independent process management and streaming output. We implemented a dedicated grain for each tool—&lt;code&gt;ClaudeCodeGrain&lt;/code&gt;, &lt;code&gt;CodexGrain&lt;/code&gt;, &lt;code&gt;GeminiGrain&lt;/code&gt;, etc., listing them like roll call. Then we rely on a factory for unified routing:&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;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExecutorGrainFactory&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IExecutorGrainFactory&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IExecutorStreamGrain&lt;/span&gt; &lt;span class="nf"&gt;GetExecutorGrain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;AIProviderType&lt;/span&gt; &lt;span class="n"&gt;executorType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CessionId&lt;/span&gt; &lt;span class="n"&gt;cessionId&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="n"&gt;executorType&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;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClaudeCodeCli&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ExecutorStreamGrainAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&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;IClaudeCodeGrain&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;cessionId&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="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CodexCli&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ExecutorStreamGrainAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&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;ICodexGrain&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;cessionId&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="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GeminiCli&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ExecutorStreamGrainAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&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;IGeminiGrain&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;cessionId&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="c1"&gt;// ... 10+ providers&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NotSupportedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;$"Unsupported executor type: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;executorType&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="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;All executor grains implement the same &lt;code&gt;IExecutorStreamGrain&lt;/code&gt; interface, with unified adaptation via &lt;code&gt;ExecutorStreamGrainAdapter&lt;/code&gt;. Upper-layer code is completely unaware of which Provider is being used underneath—add a new tool? Add a new grain class, add one line to the factory switch, done. This extension point—how to put it—is like leaving a door for your future self, and behind the door is no complex maze, just walk straight in.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Streaming Communication Pipeline
&lt;/h3&gt;

&lt;p&gt;Orleans's native support for &lt;code&gt;IAsyncEnumerable&amp;lt;T&amp;gt;&lt;/code&gt; makes streaming output particularly natural. Taking &lt;code&gt;ClaudeCodeGrain&lt;/code&gt; as an 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="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;ClaudeCodeResponse&lt;/span&gt;&lt;span class="p"&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="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;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="p"&gt;{&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;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;configuration&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;CreateProviderAsync&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="n"&gt;token&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="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&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="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;response&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 entire pipeline looks like this: CLI process stdout → grain streaming yield → &lt;code&gt;ExecutorGrainFactory&lt;/code&gt; wraps as &lt;code&gt;SessionMessage&lt;/code&gt; → &lt;code&gt;SessionGrain&lt;/code&gt; pushes to frontend via SignalR. Every step is async streaming, no intermediate buffering, no synchronous blocking. This is also one of the best parts about Orleans compared to traditional approaches—you don't need to maintain a &lt;code&gt;ConcurrentQueue&lt;/code&gt; inside the grain and manually push, &lt;code&gt;yield return&lt;/code&gt; four characters handle everything. This fluency—once you've experienced it, there's no going back.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Layered Timeout Strategy
&lt;/h3&gt;

&lt;p&gt;AI operations have extreme time variance—a simple syntax correction might take 3 seconds, a complex refactoring could run for two hours. One-size-fits-all timeout strategy? What gets cut isn't the knife.&lt;/p&gt;

&lt;p&gt;We configure in layers: Silo level defaults to 30 second timeout, individual interfaces override via &lt;code&gt;[ResponseTimeout]&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;GrainTimeouts&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;LongRunningResponseTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"02:00:00"&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;HealthCheckResponseTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"00:01:00"&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="nf"&gt;Alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"HagiCode.Orleans.IAIGrain"&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;interface&lt;/span&gt; &lt;span class="nc"&gt;IAIGrain&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="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ResponseTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GrainTimeouts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LongRunningResponseTimeout&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;lt;&lt;/span&gt;&lt;span class="n"&gt;ProposalOptimizationBundleResultDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;OptimizeProposalBundleAsync&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ResponseTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GrainTimeouts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HealthCheckResponseTimeout&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;lt;&lt;/span&gt;&lt;span class="n"&gt;HealthCheckResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;PingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HealthCheckRequest&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="k"&gt;null&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 principle is simple: conservative by default, relaxed as needed. This isn't some deep theory—it's just applying the principle of least privilege to timeout configuration. AI operations get two hours, health checks get one minute, each lives their own life, no one delays anyone.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Batch Grain Collection Configuration
&lt;/h3&gt;

&lt;p&gt;By default, Orleans automatically recycles (deactivates) grains after they're idle for a while. This is good in itself, but frequent activation/recycling is like repeatedly opening and closing the fridge door—just adding overhead. We configured longer collection times uniformly for core grain types:&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;void&lt;/span&gt; &lt;span class="nf"&gt;ConfigureGrainCollectionOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;GrainCollectionOptions&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;OrleansTimeoutPolicy&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;timeoutPolicy&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;coreGrainTypes&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="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;SessionGrain&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;FullName&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;ClaudeCodeGrain&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;FullName&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;CodexGrain&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;FullName&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;GameDriverGrain&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;FullName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// ... 十余种核心 grain&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;collectionAge&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timeoutPolicy&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;GrainCollectionAge&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;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;24&lt;/span&gt;&lt;span class="p"&gt;);&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;name&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;coreGrainTypes&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;ClassSpecificCollectionAge&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;!]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;collectionAge&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// MessageBucket exception: 10 minute fast recycling&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;ClassSpecificCollectionAge&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;MessageBucketGrain&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;FullName&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;FromMinutes&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core idea is differentiation: high-frequency short-lived grains recycle quickly to release memory, core business grains keep hot caches with minimal churn. This optimization looks simple, but if you don't set it, default collection strategy has visible impact on throughput—anyone who's wrestled with this knows what I mean.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Local Development and Persistence
&lt;/h3&gt;

&lt;p&gt;HagiCode local development uses Development Clustering, persistence via SQLite Shard, already validated across multiple contributor environments:&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="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOrleans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;siloBuilder&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;siloBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseDevelopmentClustering&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;=&amp;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;PrimarySiloEndpoint&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;IPEndPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;IPAddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Loopback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;siloPort&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;siloBuilder&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ClusterOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;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;ClusterId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"hagicode-cluster"&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;ServiceId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"hagicode-service"&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="nf"&gt;AddActivityPropagation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;siloBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSqliteGrainStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;ProviderConstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEFAULT_STORAGE_PROVIDER_NAME&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;=&amp;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;ShardRootPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storageOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShardRootPath&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;ShardCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storageOptions&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;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseWalMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storageOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseWalMode&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;Custom &lt;code&gt;SqliteGrainStorage&lt;/code&gt; creates multiple database files by shard, paths like &lt;code&gt;data/orleans/grains/shard_00.db&lt;/code&gt;. Production can switch to Azure Table Storage or SQL Server without changing a single line of code—this is the benefit of Orleans's storage provider abstraction. How to put it—good abstractions let you change backends like changing clothes; bad abstractions make it as painful as changing skin.&lt;/p&gt;

&lt;h3&gt;
  
  
  Concurrent Session Control
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;SessionConcurrencyManager&lt;/code&gt; uses in-process locks + global counters to manage active session count limits:&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;SessionConcurrencyManager&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;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SessionId&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GlobalActiveSessions&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;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Lock&lt;/span&gt; &lt;span class="n"&gt;Lock&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;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;ConcurrencyCheckResult&lt;/span&gt; &lt;span class="nf"&gt;TryActivateSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SessionId&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Lock&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;GlobalActiveSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&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;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ConcurrencyCheckResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Allowed&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GlobalActiveSessions&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;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;_cachedMaxConcurrentSessions&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;ConcurrencyCheckResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Allowed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

            &lt;span class="n"&gt;GlobalActiveSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&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;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ConcurrencyCheckResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Allowed&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="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 manager uses Stack Trace + Caller verification to restrict calls only from inside &lt;code&gt;SessionGrain&lt;/code&gt;, preventing external code from bypassing concurrency checks. Though honestly, using &lt;code&gt;internal static&lt;/code&gt; here actually breaks Actor isolation principles—but concurrency control is indeed a global need, so after weighing tradeoffs we accepted this design compromise. Perfect is the enemy of good—this saying holds true in architectural design too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Health Check Integration
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;AIGrain.PingAsync()&lt;/code&gt; has two modes: lightweight connectivity probing and explicit Ping-Pong verification. The latter is used in the setup wizard to verify if a Provider is actually usable:&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="n"&gt;HealthCheckResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;PingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;HealthCheckRequest&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="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;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;isModelAware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Lightweight CLI readiness probe&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;provider&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;aiProviderFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetProviderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClaudeCodeCli&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;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeoutCts&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HealthCheckResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;IsHealthy&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="n"&gt;Success&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Explicit Ping-Pong verification&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&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;aiService&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="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;AIRequest&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;HealthCheckPingPongProbe&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;SystemMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HealthCheckPingPongProbe&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="n"&gt;Temperature&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;MaxTokens&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;32&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;timeoutCts&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HealthCheckPingPongProbe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsExpectedResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;normalizedResponse&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;HealthCheckResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;IsHealthy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;passed&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;Temperature set to 0, MaxTokens limited to 32—guarantees response determinism while controlling costs. After all, health checks aren't for running benchmarks, just needs to be good enough. Same with people—knowing when to stop is rarer than knowing when to act.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Looking back at HagiCode's journey building backend systems with Orleans, five core design decisions are worth remembering:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Configure timeouts at interface granularity&lt;/strong&gt;, don't use global unified timeouts—AI operations 2h, health checks 1min, default 30s, each manages their own, no crossing paths.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Differentiate Grain Collection ages&lt;/strong&gt;—high-frequency short-lived grains recycle quickly, core business grains keep hot caches, be fast where needed, be stable where needed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Streaming pipeline should be async throughout&lt;/strong&gt;—from CLI stdout to SignalR push, don't introduce any synchronous blocking middleware, let it flow naturally like water.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Facade Grain splits complexity&lt;/strong&gt;—components are stateless but share persistent state, much more maintainable than god classes. Divide and conquer—ancestor wisdom applies equally well to code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mark stable names on Grain interfaces with &lt;code&gt;[Alias]&lt;/code&gt;&lt;/strong&gt;—the last line of defense for serialization compatibility. Guard this line, and the probability of being woken up by alerts in the middle of the night drops significantly.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Orleans's Virtual Actor model provides a complete-to-the-point-of-touching runtime abstraction for stateful, long-lifecycle session systems. If you're also building similar AI workbenches or real-time collaboration systems, this solution is worth trying—not because it's perfect, but because in the right scenarios, it's just right.&lt;/p&gt;

&lt;p&gt;This memory can become something to cherish, though I was bewildered at the time... getting off track. Anyway, the code runs, the article is written. That's it.&lt;/p&gt;

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

&lt;p&gt;Around "Solving Backend Distributed Challenges in AI Programming Workbenches with Orleans," a more prudent approach is to first get key configurations, dependency boundaries, and implementation paths working, then fill in optimization details.&lt;/p&gt;

&lt;p&gt;When goals, steps, and acceptance criteria are clear, such solutions typically transition more smoothly into actual delivery.&lt;/p&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-06-21-orleans-hagicode-backend-system%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-21-orleans-hagicode-backend-system%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>orleans</category>
      <category>hagicode</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>What is MonoSpecs: Why It's a Further Upgrade and Extension of OpenSpec</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Sat, 20 Jun 2026 04:30:21 +0000</pubDate>
      <link>https://dev.to/newbe36524/what-is-monospecs-why-its-a-further-upgrade-and-extension-of-openspec-36e5</link>
      <guid>https://dev.to/newbe36524/what-is-monospecs-why-its-a-further-upgrade-and-extension-of-openspec-36e5</guid>
      <description>&lt;h1&gt;
  
  
  What is MonoSpecs: Why It's a Further Upgrade and Extension of OpenSpec
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;When a product system expands to 40+ independent Git repositories, where should "specifications" be placed? This article discusses the two steps HagiCode took in multi-repository governance: first lifting OpenSpec to the main repository, then developing MonoSpecs as a multi-repository management solution on top of it. Really nothing special, just stumbled through some pitfalls and wanted to document them.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Anyone who's worked on a slightly larger product has probably had this experience—initially the code was in a single repository, well-organized and peaceful; later the frontend, backend, desktop app, documentation site, official website, and build tools each became independent repositories, and the number of repositories kept growing like wild grass, unstoppable. Then when you wanted to write a "specification document" for some cross-repository functionality, suddenly you didn't know where to write it—it's a bit like childhood pocket money, suddenly gone without a trace.&lt;/p&gt;

&lt;p&gt;Our own HagiCode is such a product system composed of 40+ independent Git repositories. In the early days, we directly stuffed OpenSpec's &lt;code&gt;openspec/&lt;/code&gt; directory into the backend sub-repository &lt;code&gt;hagicode-core&lt;/code&gt;, thinking that since the backend is the core, placing it there would be most stable. As the number of repositories grew, this approach exposed a series of headache-inducing problems. After all, the code world never actually becomes stable just because you "thought it would be stable."&lt;/p&gt;

&lt;p&gt;The first pain point: &lt;strong&gt;specs are trapped in a single sub-repository&lt;/strong&gt;. If a functionality affects both the frontend &lt;code&gt;web&lt;/code&gt; and the backend &lt;code&gt;hagicode-core&lt;/code&gt;, I have to write proposals in &lt;code&gt;hagicode-core&lt;/code&gt;, then go to other sub-repositories to execute code changes. Which repository a proposal belongs to itself becomes a controversy.&lt;/p&gt;

&lt;p&gt;The second pain point: &lt;strong&gt;sub-repositories are not pure&lt;/strong&gt;. Each sub-repository carries its own &lt;code&gt;openspec/&lt;/code&gt;, mixing specification documents with product code. Someone clones your frontend repository and ends up bringing back a bunch of backend proposal documents, completely bewildered.&lt;/p&gt;

&lt;p&gt;The third pain point: &lt;strong&gt;AI Agents struggle to understand repository relationships&lt;/strong&gt;. Each sub-repository is independent, with no machine-readable "manifest" telling the AI: what repositories this product consists of, what each is responsible for, which is editable, which is read-only reference.&lt;/p&gt;

&lt;p&gt;The fourth pain point: &lt;strong&gt;high cost of cross-repository editing&lt;/strong&gt;. To modify a spec, you must first &lt;code&gt;cd&lt;/code&gt; into the corresponding sub-module, paths jump around, creating significant cognitive burden for collaboration.&lt;/p&gt;

&lt;p&gt;Against this background, we first performed an "OpenSpec Monorepo Migration," lifting specs from sub-repositories to the monorepo root directory. Then on top of it, we developed MonoSpecs as a multi-repository management solution. Understanding the progressive relationship between these two steps is the key to understanding "why MonoSpecs is a further upgrade and extension of OpenSpec."&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 with many repositories and frequent cross-language collaboration. This structural complexity forced us to make both "specifications" and "repository governance" solid. The MonoSpecs solution was gradually polished through multi-repository practice—no brilliant strokes, just a few more steps taken.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenSpec Solves "How Specifications Are Written and Evolved"
&lt;/h2&gt;

&lt;p&gt;To clarify the relationship between the two, we need to first look at what each is responsible for.&lt;/p&gt;

&lt;p&gt;OpenSpec is essentially a spec-driven change management workflow. Its core artifacts look 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;openspec/
├── specs/          # Currently active capability specifications (one spec.md per capability)
├── changes/        # In-progress proposals
│   └── archive/    # Archived historical proposals
└── project.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It answers the question: a change goes through a lifecycle of proposal, design, tasks, archive, and merges deltas into specs when archiving. This mechanism itself is unrelated to "how many repositories there are, where they are, who manages them"—it only cares about how spec files are organized.&lt;/p&gt;

&lt;p&gt;Through a migration proposal, we lifted 82+ spec files originally scattered in &lt;code&gt;hagicode-core/openspec/&lt;/code&gt; to the monorepo root's &lt;code&gt;openspec/&lt;/code&gt;, making all specs visible in one place with unified version control.&lt;/p&gt;

&lt;p&gt;But this migration was essentially just "moving spec files," and didn't answer the more fundamental question: what sub-repositories does this monorepo actually consist of? What are the relationships between these sub-repositories? This is what MonoSpecs fills in.&lt;/p&gt;

&lt;h2&gt;
  
  
  MonoSpecs Solves "How Multi-Repositories Themselves Are Managed"
&lt;/h2&gt;

&lt;p&gt;The core of MonoSpecs is a machine-readable manifest file: &lt;code&gt;.hagicode/monospecs.yaml&lt;/code&gt;. It does four things that OpenSpec doesn't address at all.&lt;/p&gt;

&lt;p&gt;The first: &lt;strong&gt;declare sub-repository manifest&lt;/strong&gt;. Each repository's path, url, displayName, icon, tags, whether collapsed to "More", all written in a single YAML, clear at a glance.&lt;/p&gt;

&lt;p&gt;The second: &lt;strong&gt;drive clone scripts&lt;/strong&gt;. &lt;code&gt;scripts/clone-repos.mjs&lt;/code&gt; reads this YAML directly, batch &lt;code&gt;git clone&lt;/code&gt;, no longer hardcoding repository lists. Adding a new repository just requires one line in the YAML, the script needs zero changes.&lt;/p&gt;

&lt;p&gt;The third: &lt;strong&gt;provide project structure context for AI/IDE&lt;/strong&gt;. Paired with &lt;code&gt;AGENTS.md&lt;/code&gt;, AI Agents can instantly see which repository is editable, which is reference-only, and what the tech stack is.&lt;/p&gt;

&lt;p&gt;The fourth: &lt;strong&gt;anchor OpenSpec artifacts to the main repository&lt;/strong&gt;. Specs are no longer scattered across sub-repositories, but unified in the main repository root's &lt;code&gt;openspec/&lt;/code&gt;, keeping sub-repositories pure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Layers of Meaning, Don't Get Confused
&lt;/h2&gt;

&lt;p&gt;In MonoSpecs' official guide, a very confusing point is clearly pointed out: MonoSpecs actually has &lt;strong&gt;two layers of meaning&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One layer is the &lt;strong&gt;configuration system layer&lt;/strong&gt;, referring to the &lt;code&gt;.hagicode/monospecs.yaml&lt;/code&gt; configuration file itself, along with its accompanying loading, validation, and caching mechanisms.&lt;/p&gt;

&lt;p&gt;The other layer is the &lt;strong&gt;repository type layer&lt;/strong&gt;, referring to a "main repository + multiple sub-repositories + centralized specs" repository organization pattern. When we say a project "is a MonoSpecs project," it means it adopts this structure.&lt;/p&gt;

&lt;p&gt;These two layers叠加 together form the complete MonoSpecs. Many people encountering it for the first time only see the YAML file layer, thinking MonoSpecs is just a configuration manifest, but its value is actually more in the second layer—a clear multi-repository collaboration paradigm. Beautiful things often aren't visible at first glance; you need to look a few more times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "Upgrade and Extension"
&lt;/h2&gt;

&lt;p&gt;Putting them side by side, the relationship becomes clear:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;OpenSpec&lt;/th&gt;
&lt;th&gt;MonoSpecs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Focus&lt;/td&gt;
&lt;td&gt;spec file content and lifecycle&lt;/td&gt;
&lt;td&gt;repository organization structure and manifest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Core artifact&lt;/td&gt;
&lt;td&gt;&lt;code&gt;openspec/specs/*/spec.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.hagicode/monospecs.yaml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Depends on the other&lt;/td&gt;
&lt;td&gt;Doesn't depend on MonoSpecs&lt;/td&gt;
&lt;td&gt;Depends on OpenSpec, reuses its &lt;code&gt;openspec/&lt;/code&gt; for change management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pain point solved&lt;/td&gt;
&lt;td&gt;How specifications are written and evolved&lt;/td&gt;
&lt;td&gt;How multi-repositories are declared, cloned, and understood by AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;Can be used in any repository&lt;/td&gt;
&lt;td&gt;Designed specifically for "one main multiple sub" multi-repository structure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fundamentally, MonoSpecs doesn't replace OpenSpec, but adds a layer of "repository governance" on top of it. Using &lt;code&gt;monospecs.yaml&lt;/code&gt; to describe repository topology, using centralized &lt;code&gt;openspec/&lt;/code&gt; to decouple specs from sub-repositories, using &lt;code&gt;commit_when_archive&lt;/code&gt; to automatically archive commits to the main repository.&lt;/p&gt;

&lt;p&gt;If using an analogy: OpenSpec provides "change syntax," MonoSpecs provides "multi-repository semantics." The former is the premise of the latter, the latter is an extension of the former. All roads lead to Rome, just this time, the road is a bit longer than imagined.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Implement: Four Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Establish Main Repository and Configuration File
&lt;/h3&gt;

&lt;p&gt;Place the configuration file in the monorepo root directory, declaring all sub-repositories. Taking our own project as an example, the structure is roughly like this:&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="c1"&gt;# .hagicode/monospecs.yaml&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0"&lt;/span&gt;
&lt;span class="na"&gt;commit_when_archive&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;repositories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repos/web"&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/HagiCode-org/web.git"&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Frontend"&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;react&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pcode-client&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repos/hagicode-core"&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/newbe36524/pcode"&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Backend"&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;dotnet&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;orleans&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repos/docs"&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/HagiCode-org/docs.git"&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Docs"&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;docs&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;astro&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;starlight&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;ui&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;collapseToMore&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;# Collapse to "More" in the UI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few fields to pay special attention to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;path&lt;/code&gt; is the local path relative to the main repository root, and is also the unique key for each record.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;url&lt;/code&gt; is the Git remote address, which the clone script relies on to pull code.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;displayName&lt;/code&gt; / &lt;code&gt;icon&lt;/code&gt; / &lt;code&gt;tags&lt;/code&gt; only affect UI display and AI context, not clone behavior.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;commit_when_archive: true&lt;/code&gt; automatically commits to the main repository when OpenSpec proposals are archived.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Lift OpenSpec to Main Repository Root Directory
&lt;/h3&gt;

&lt;p&gt;Before and after migration comparison:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before migration (specs trapped in sub-repository)         After migration (specs centralized in main repository)
hagicode-core/                                            .  (main repository root)
└── openspec/                                             ├── .hagicode/monospecs.yaml
    └── specs/  (82+ specs)                               ├── openspec/
                                                          │   ├── specs/   (centralized management)
                                                          │   └── changes/
                                                          └── repos/
                                                              ├── hagicode-core/  (pure, no openspec)
                                                              ├── web/
                                                              └── docs/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From then on, sub-repositories no longer carry &lt;code&gt;openspec/&lt;/code&gt;, and the main repository becomes the sole source of truth for specs. This step looks simple, but the benefits it brings are very practical—any engineer standing at the main repository root can see all specifications for the entire product system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Make Clone Scripts Read Configuration Instead of Hardcoding
&lt;/h3&gt;

&lt;p&gt;The core logic of &lt;code&gt;scripts/clone-repos.mjs&lt;/code&gt; is to read YAML and clone one by one:&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;const&lt;/span&gt; &lt;span class="nx"&gt;CONFIG_PATH&lt;/span&gt; &lt;span class="o"&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;__dirname&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;.hagicode&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;monospecs.yaml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Parse repositories array&lt;/span&gt;
&lt;span class="c1"&gt;// Execute git clone &amp;lt;url&amp;gt; &amp;lt;path&amp;gt; for each entry&lt;/span&gt;
&lt;span class="c1"&gt;// Skip or git pull if target directory already exists&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When adding a new repository, you only need to add one line in the YAML, without touching the script. This small change saves countless "forgot to sync repository list" disputes. After all, who wants to repeat labor?&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Backend Provides Unified MonoSpecs Service Layer
&lt;/h3&gt;

&lt;p&gt;If you don't extract an abstraction layer, configuration parsing logic can easily scatter across &lt;code&gt;GitAppService&lt;/code&gt;, &lt;code&gt;ProjectAppService&lt;/code&gt; and other corners. HagiCode extracted &lt;code&gt;IMonoSpecsService&lt;/code&gt; in the &lt;code&gt;ClaudeHelper&lt;/code&gt; module, exposing a set of clear 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;IMonoSpecsService&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;lt;&lt;/span&gt;&lt;span class="n"&gt;MonoSpecsConfigDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetConfigAsync&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;projectPath&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;lt;&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RepositoryInfoDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetSubRepositoriesAsync&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;projectPath&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;lt;&lt;/span&gt;&lt;span class="n"&gt;MonoSpecsDataDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetMonoSpecsDataAsync&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;projectPath&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;lt;&lt;/span&gt;&lt;span class="n"&gt;MonoSpecsManagementDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetManagementDocumentAsync&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;projectPath&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;lt;&lt;/span&gt;&lt;span class="n"&gt;MonoSpecsManagementDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;InitializeManagementDocumentAsync&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;projectPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ValidateManagementDocumentAsync&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;projectPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UpdateMonoSpecsManagementRequestDto&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;Task&lt;/span&gt; &lt;span class="nf"&gt;SaveManagementDocumentAsync&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;projectPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UpdateMonoSpecsManagementRequestDto&lt;/span&gt; &lt;span class="n"&gt;request&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 service is responsible for loading, validating, caching configuration, and providing "initialize minimal template" capability—one-click generation of &lt;code&gt;monospecs.yaml&lt;/code&gt;, &lt;code&gt;repos/&lt;/code&gt;, &lt;code&gt;openspec/changes/archive/&lt;/code&gt;, &lt;code&gt;openspec/specs/&lt;/code&gt; skeleton for an empty project, and automatically completing &lt;code&gt;.gitignore&lt;/code&gt;. Caching, like memory, once remembered, next time you don't have to think hard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Several Pitfalls in Practice
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Initializing a Brand New MonoSpecs Project
&lt;/h3&gt;

&lt;p&gt;After calling &lt;code&gt;InitializeManagementDocumentAsync&lt;/code&gt;, this structure will appear on disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-project/
├── .gitignore               # Added repos/ ignore rules (idempotent, won't duplicate append)
├── .hagicode/
│   └── monospecs.yaml       # Minimal template: version / commit_when_archive / repositories: []
├── openspec/
│   ├── changes/archive/
│   └── specs/
└── repos/                   # Empty directory, waiting for clone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few boundaries to pay attention to here, all extracted from specs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Idempotent&lt;/strong&gt;: Existing &lt;code&gt;repos/&lt;/code&gt;, &lt;code&gt;openspec/&lt;/code&gt; directories will be preserved and won't error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No overwrite&lt;/strong&gt;: If &lt;code&gt;monospecs.yaml&lt;/code&gt; already exists and can be parsed normally, initialization won't touch it, only supplements missing &lt;code&gt;.gitignore&lt;/code&gt; rules and openspec directories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reject dirty config&lt;/strong&gt;: Existing but unparsable &lt;code&gt;monospecs.yaml&lt;/code&gt; will be directly rejected, returning diagnosable error information, absolutely no overwrite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No auto-scan&lt;/strong&gt;: Initialization won't scan disk directories into repository entries on its own, &lt;code&gt;repositories&lt;/code&gt; defaults to empty, requiring manual or UI filling.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration File Location Migration Trap
&lt;/h3&gt;

&lt;p&gt;Historically, &lt;code&gt;monospecs.yaml&lt;/code&gt; was once placed in the project root, later forcibly migrated to &lt;code&gt;.hagicode/monospecs.yaml&lt;/code&gt;. This is clearly stated in the spec:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Root directory's &lt;code&gt;monospecs.yaml&lt;/code&gt; is no longer detected, nor as a compatibility fallback. Clone scripts only recognize &lt;code&gt;.hagicode/monospecs.yaml&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So when upgrading old projects, you must manually execute &lt;code&gt;mv monospecs.yaml .hagicode/monospecs.yaml&lt;/code&gt;, with no silent compatibility path. At first glance it seems unreasonable, but thinking carefully, this is to completely eliminate the ambiguity of "both locations might take effect"—once this ambiguity exists, troubleshooting can drive people crazy. After all, who wants to look for answers between two files?&lt;/p&gt;

&lt;h3&gt;
  
  
  Save Validation: Don't Write Invalid Configuration
&lt;/h3&gt;

&lt;p&gt;Before writing back through &lt;code&gt;SaveManagementDocumentAsync&lt;/code&gt;, the service performs field-level validation. Several typical rejection scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two repository entries with duplicate &lt;code&gt;path&lt;/code&gt; → reject, return conflicting fields.&lt;/li&gt;
&lt;li&gt;Any entry missing &lt;code&gt;path&lt;/code&gt; → reject, return required field error.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;url&lt;/code&gt; is non-empty but not a valid absolute URL → reject.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after validation passes will it serialize to YAML and write to disk, while invalidating that project path's configuration cache, ensuring the next read gets the latest content. This step looks trivial, but can avoid countless "why didn't my configuration changes take effect" tickets. After all, when these tickets accumulate, who can bear it?&lt;/p&gt;

&lt;h3&gt;
  
  
  Workspace Mode vs Manual Repositories Mode
&lt;/h3&gt;

&lt;p&gt;The configuration file supports two ways to derive repository lists.&lt;/p&gt;

&lt;p&gt;One is &lt;strong&gt;manual repositories mode&lt;/strong&gt;, directly listing each repository in the YAML, management document marked as editable.&lt;/p&gt;

&lt;p&gt;The other is &lt;strong&gt;workspace mode&lt;/strong&gt;, declaring a &lt;code&gt;.code-workspace&lt;/code&gt; file, which derives the repository list. In this mode the management document is marked as read-only, prohibiting direct modification of the repository array, only modifying supported top-level fields.&lt;/p&gt;

&lt;p&gt;Our own HagiCode Mono currently has workspace mode commented out, using manual mode. The reason is simple: manual mode allows fine-grained control of each repository's icon and tags, making UI display effects more controllable. As they say, things you can control always make you feel more at ease.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Advice for AI Agents
&lt;/h2&gt;

&lt;p&gt;Now with AI programming becoming increasingly popular, the MonoSpecs solution actually has an implicit value: it provides AI with a structured project map.&lt;/p&gt;

&lt;p&gt;In multi-repository collaboration, &lt;code&gt;AGENTS.md&lt;/code&gt; and &lt;code&gt;monospecs.yaml&lt;/code&gt; are two key contexts for AI. The recommended workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First read &lt;code&gt;monospecs.yaml&lt;/code&gt; to get repository topology, understanding who is editable, who is reference-only.&lt;/li&gt;
&lt;li&gt;Then read root &lt;code&gt;AGENTS.md&lt;/code&gt;'s "Active Edit Scope" to confirm the currently allowed modification scope.&lt;/li&gt;
&lt;li&gt;Cross-repository changes uniformly write proposals in the main repository root's &lt;code&gt;openspec/changes/&lt;/code&gt;, don't start separate openspec in each sub-repository.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This convention lets AI stably understand the division of "main repository manages specs, sub-repositories manage code," and won't mistakenly write specs into sub-repositories—we've stumbled into this pit several times before. Actually can't blame AI, after all sub-repositories and main repositories look so similar, who can tell at a glance?&lt;/p&gt;

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

&lt;p&gt;One sentence summary: &lt;strong&gt;OpenSpec defines "how changes are written," MonoSpecs defines "how repositories are placed."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The former is the grammatical foundation of the latter, the latter extends the former from single-repository context to multi-repository context, and converges repository topology, clone process, AI context, and specs ownership in one go with a YAML manifest. This is the true meaning of "MonoSpecs is a further upgrade and extension of OpenSpec"—not replacement, but adding a layer of multi-repository semantics on top of it.&lt;/p&gt;

&lt;p&gt;If you're also working on a multi-repository product of similar scale, consider whether these two layers are both laid out properly. No matter how beautifully specifications are written, without clear repository governance supporting them, eventually it will all become a chaotic mess......&lt;/p&gt;

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

&lt;ul&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/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode-org/site GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Fission-AI/OpenSpec" rel="noopener noreferrer"&gt;OpenSpec Workflow Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;MonoSpecs related specs: &lt;code&gt;monospecs-guide&lt;/code&gt;, &lt;code&gt;monospecs-repository-config&lt;/code&gt;, &lt;code&gt;monospec-config-management&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Surrounding "What is MonoSpecs: Why It's a Further Upgrade and Extension of OpenSpec," a more prudent advancement approach is to first gradually run through key configurations, dependency boundaries, and implementation paths, then supplement optimization details.&lt;/p&gt;

&lt;p&gt;Once goals, steps, and acceptance criteria are clear, such solutions can usually more smoothly enter actual delivery.&lt;/p&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-06-20-monospecs-openspec-upgrade-and-extension%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-20-monospecs-openspec-upgrade-and-extension%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>openspec</category>
      <category>monospecs</category>
      <category>hagicode</category>
    </item>
    <item>
      <title>Prompts for AI Commits in HagiCode: Design Rationale and Implementation Breakdown</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Sat, 20 Jun 2026 04:29:48 +0000</pubDate>
      <link>https://dev.to/newbe36524/prompts-for-ai-commits-in-hagicode-design-rationale-and-implementation-breakdown-1dha</link>
      <guid>https://dev.to/newbe36524/prompts-for-ai-commits-in-hagicode-design-rationale-and-implementation-breakdown-1dha</guid>
      <description>&lt;h1&gt;
  
  
  Prompts for AI Commits in HagiCode: Design Rationale and Implementation Breakdown
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;When you dump a bunch of messy changes to AI and ask it to help you commit, what exactly is the prompt being sent to the model behind the scenes? Why is the prompt written that way? This article breaks down the actual prompt that drives "AI commits" in HagiCode for you to see.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Using AI to assist with development—after a full day of coding, it can be quite exhausting. You've accumulated a pile of uncommitted changes: config files, documentation, business logic, test cases all mixed together—just looking at it gives you a headache. Manually grouping them, hand-writing commit messages that conform to standards, then switching branches and pushing—these "cleanup tasks" alone can eat up half an hour.&lt;/p&gt;

&lt;p&gt;Naturally, this gives rise to a demand—&lt;strong&gt;can we just throw all uncommitted changes at AI at once, let it analyze, group, write messages, and even directly commit + push?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The idea is good, but there are plenty of pitfalls along the way. AI might only change &lt;code&gt;--author&lt;/code&gt; without changing Committer, resulting in the correct author but wrong committer in commit history—a jarring inconsistency. It might write flashy messages that don't align with your repository's style at all. It might arbitrarily switch to the main branch and mess things up. It might miss &lt;code&gt;Co-Authored-By&lt;/code&gt; or recklessly add &lt;code&gt;Signed-off-by&lt;/code&gt;, triggering compliance issues.&lt;/p&gt;

&lt;p&gt;These pitfalls are lessons learned the hard way. To address these pain points, we've made "AI commit" a parameterized Agent task contract. What this contract looks like and why it's designed this way is what this article aims to clarify.&lt;/p&gt;

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

&lt;p&gt;The solution shared in this article comes from our practice in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode is an AI code assistant focused on developer workflows, turning daily tasks like Git commits, code reviews, and build deployments into AI-participated tasks. The prompt system broken down below is the one actually running in HagiCode's backend. Ultimately, it's just about handing off those tedious "cleanup tasks" to AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The True Form of Prompts: Templates Plus Metadata, Not a Hardcoded String
&lt;/h2&gt;

&lt;p&gt;Many people think "prompts" are just a piece of hardcoded natural language to throw at a model. But HagiCode's approach is completely different.&lt;/p&gt;

&lt;p&gt;The actual prompt driving "AI commits" is called &lt;code&gt;auto-compose-commit&lt;/code&gt;, corresponding to &lt;code&gt;PromptScenario.AutoComposeCommit&lt;/code&gt; in the code. It's located under &lt;code&gt;repos/hagicode-core/src/PCode.Web/Resources/Prompts/&lt;/code&gt; with this structure:&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/
├── auto-compose-commit.en-US.hbs     # English Handlebars template
├── auto-compose-commit.en-US.json    # English metadata (parameter schema, version, tags)
├── auto-compose-commit.zh-CN.hbs     # Chinese template
└── auto-compose-commit.zh-CN.json    # Chinese metadata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In other words, a prompt is a combination of &lt;strong&gt;one Handlebars template + one JSON metadata&lt;/strong&gt;, flattened into multiple sets by locale.&lt;/p&gt;

&lt;p&gt;Why split it this way? There are several considerations behind it.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;metadata decoupled from prompt body&lt;/strong&gt;. JSON describes the parameter schema—parameter names, types, whether required, default values. &lt;code&gt;.hbs&lt;/code&gt; only handles "how to say it." This way, the frontend can automatically render the correct input form based on JSON without knowing the template content: Git identity selector, Co-Authored-By mode, target branch strategy, whether to push... these controls are all driven by JSON.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;multi-language flattened, not using i18n keys for translation&lt;/strong&gt;. Each locale has a complete set of &lt;code&gt;.hbs&lt;/code&gt; + &lt;code&gt;.json&lt;/code&gt;, avoiding "translation key drift." Different languages don't just substitute words—grouping examples and command examples can also be localized. Commit habits in Chinese and English repositories are inherently different; forcing them into one template then translating feels awkward.&lt;/p&gt;

&lt;p&gt;Third, &lt;strong&gt;migrating from Scriban to Handlebars was for performance&lt;/strong&gt;. &lt;code&gt;HandlebarsTemplateRenderer&lt;/code&gt; chose &lt;code&gt;Handlebars.Net&lt;/code&gt; because it can "compile templates directly to IL bytecode," which is much faster than interpretation. During migration, an interesting compatibility fix was made: replacing &lt;code&gt;True/False&lt;/code&gt; with &lt;code&gt;true/false&lt;/code&gt; in render results to match old Scriban boolean output habits—without paying attention to such details, old tests would all fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five Key Decisions Behind This Prompt Structure
&lt;/h2&gt;

&lt;p&gt;Looking at &lt;code&gt;auto-compose-commit.zh-CN.hbs&lt;/code&gt;, the skeleton roughly is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Non-interactive mode instructions
├── &amp;lt;task&amp;gt;            Task definition: analyze changes, intelligent grouping, multiple commits
├── &amp;lt;context&amp;gt;         Context: projectPath + push control + target branch control
├── &amp;lt;working_directory&amp;gt;
├── &amp;lt;git_profile&amp;gt;     Identity: Author plus Committer dual-write
├── &amp;lt;tools&amp;gt;           Tool whitelist
├── &amp;lt;requirements&amp;gt;    Hard requirements (branch, grouping, Co-Authored-By, Signed-off-by, Conventional Commits)
├── &amp;lt;historical_format_analysis&amp;gt;  Historical consistency
├── &amp;lt;constraints&amp;gt;     Constraints (prohibit reset, ignore .gitignore)
├── &amp;lt;workflow&amp;gt;        Step-by-step execution flow
├── &amp;lt;output_format&amp;gt;   Strict `---` separated output
└── &amp;lt;final_instruction&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below, let's expand on five points that best reflect the design intent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision One: Execute Directly, Don't Just Generate a Plan
&lt;/h3&gt;

&lt;p&gt;The prompt repeatedly emphasizes one sentence: &lt;strong&gt;Use Git commands directly to execute each commit, don't return a plan, operate directly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the fundamental difference between "Auto Compose Commit" and earlier solutions. The early &lt;code&gt;ai-git-commit-message-generator&lt;/code&gt; (corresponding to the &lt;code&gt;ai-commit-message-generation&lt;/code&gt; spec in OpenSpec) only did one thing: call a &lt;code&gt;POST /api/git/generate-commit-message&lt;/code&gt;, return a commit message string, and let the user manually commit the rest.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;auto-compose-commit&lt;/code&gt; is different. It's an &lt;strong&gt;Agent automated task&lt;/strong&gt;. The model must call &lt;code&gt;Bash(git:*)&lt;/code&gt; tools itself and run the full chain of add → commit → push. This difference determines the tone of the entire prompt—it can't just describe "what kind of message to write," but also specify "what workflow to follow, what tools to use, and what to do when errors occur."&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision Two: Why Git Identity is So Verbose
&lt;/h3&gt;

&lt;p&gt;There's a large section in &lt;code&gt;&amp;lt;git_profile&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;requirements&amp;gt;&lt;/code&gt; explaining Author versus Committer, which looks redundant at first glance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;- &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nt"&gt;--author&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Name &amp;lt;email&amp;gt;"&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; only modifies Author
- &lt;span class="sb"&gt;`&lt;/span&gt;git &lt;span class="nt"&gt;-c&lt;/span&gt; user.name&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Name"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; user.email&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt; commit ...&lt;span class="sb"&gt;`&lt;/span&gt; only modifies Committer &lt;span class="k"&gt;for &lt;/span&gt;this &lt;span class="nb"&gt;command&lt;/span&gt;
- For each generated commit, you must &lt;span class="nb"&gt;set &lt;/span&gt;both Author and Committer to the selected identity
- Preferred &lt;span class="nb"&gt;command &lt;/span&gt;form:
  git &lt;span class="nt"&gt;-c&lt;/span&gt; user.name&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; user.email&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt; commit &lt;span class="nt"&gt;--author&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"... &amp;lt;...&amp;gt;"&lt;/span&gt; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This actually comes from real pitfalls learned. Git commits have two identity fields. Models easily only change &lt;code&gt;--author&lt;/code&gt;, leaving Committer as the global config identity. In commit history, "author is correct but committer is wrong"—visually jarring. So the prompt directly provides the preferred command template and requires the model to self-check using &lt;code&gt;git log --format=fuller -1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;By analogy, this is like shipping a package—"sender" and "actual handler" are two different forms. You only write your name on one form, the other still has the company name printed—the package ships, but records don't match, ultimately feeling awkward.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision Three: Grouping Decision Tree Plus Historical Consistency
&lt;/h3&gt;

&lt;p&gt;Models excel at "free improvisation," but in commit grouping, this is often a disaster. So the prompt provides a clear decision tree: config files as one group, docs as another, same-module code changes merged, cross-module changes handled case-by-case. It also includes positive examples, like &lt;code&gt;src/auth/login.ts&lt;/code&gt; plus &lt;code&gt;auth.service.ts&lt;/code&gt; should go into the same commit.&lt;/p&gt;

&lt;p&gt;More critical is the &lt;code&gt;&amp;lt;historical_format_analysis&amp;gt;&lt;/code&gt; section. It requires the model to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Use &lt;code&gt;git log -n 15 --pretty=format:"%H|%s|%b%n---%n"&lt;/code&gt; to get recent commit history&lt;/li&gt;
&lt;li&gt;Analyze structural patterns, language patterns, common types, special formats&lt;/li&gt;
&lt;li&gt;Generate commit messages following detected patterns&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words, the model can't write however it wants—it must align with the target repository's existing style first. HagiCode Mono main repo uses English + Conventional Commits, certain sub-repos use Chinese paragraph style—AI must follow local customs. This capability corresponds to archived proposal &lt;code&gt;2026-02-23-auto-commit-compose-history-consistency-optimization&lt;/code&gt;, an optimization added later. After all, nobody wants their commit history to look like a hodgepodge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision Four: Conditional Rendering for Co-Authored-By and Signed-off-by
&lt;/h3&gt;

&lt;p&gt;The prompt has many nested &lt;code&gt;{{#if}}&lt;/code&gt; statements, deciding whether to add trailers based on runtime parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When &lt;code&gt;coAuthoredByIsNone&lt;/code&gt;, don't add &lt;code&gt;Co-Authored-By&lt;/code&gt; at all&lt;/li&gt;
&lt;li&gt;When &lt;code&gt;coAuthoredByIsCustom&lt;/code&gt;, use the user-provided custom trailer&lt;/li&gt;
&lt;li&gt;When &lt;code&gt;signedOffByEnabled&lt;/code&gt; plus &lt;code&gt;gitProfileName&lt;/code&gt;, add &lt;code&gt;Signed-off-by&lt;/code&gt;; missing identity must error, not fabricate one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trailers involve attribution and compliance (DCO sign-off), must be explicitly controlled by users, never left to model discretion. HagiCode has implemented proposals like &lt;code&gt;git-commit-coauthor-standardization&lt;/code&gt; and &lt;code&gt;ai-commit-consent-management&lt;/code&gt; to draw these boundaries clearly. Such things are better strict than vague.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision Five: &lt;code&gt;---&lt;/code&gt; Separated Output Contract
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;output_format&amp;gt;&lt;/code&gt; specifies that each return must use &lt;code&gt;---&lt;/code&gt; to separate multiple commit blocks, with hard-coded format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
Commit 1: {hash}
{message}
---
Commit 2: {hash}
{message}
---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't for looks. A model might produce N commits in one task—the backend relies on this separator to parse each commit's hash and message, then pass them back to frontend for display. Once the output protocol loosens, backend parsing crashes. So the &lt;code&gt;---&lt;/code&gt; rule is emphasized twice in &lt;code&gt;&amp;lt;output_format&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;final_instruction&amp;gt;&lt;/code&gt;—important things should indeed be said three times.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Prompts Are Assembled and Delivered
&lt;/h2&gt;

&lt;p&gt;Looking at templates isn't enough; we need to know how they run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Loading and Rendering
&lt;/h3&gt;

&lt;p&gt;The backend registers two singletons in &lt;code&gt;PCodeClaudeHelperModule&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="c1"&gt;// Register prompt loader: find corresponding .json and .hbs by scenario + locale&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&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;IPromptLoader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FilePromptLoaderV2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// Register Handlebars renderer: compile templates to IL and cache&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&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;HandlebarsTemplateRenderer&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;&lt;code&gt;FilePromptLoaderV2&lt;/code&gt; gets the template body, then hands it to &lt;code&gt;HandlebarsTemplateRenderer.Render(template, parameters)&lt;/code&gt; for rendering. The renderer's core logic roughly is:&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="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Render&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;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IDictionary&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="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="c1"&gt;// Cache by SHA256 of template content, avoid recompiling each commit&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;compiledTemplate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetOrCompileTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&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;rendered&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compiledTemplate&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="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="c1"&gt;// Compatible with old Scriban boolean output habit&lt;/span&gt;
    &lt;span class="n"&gt;rendered&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rendered&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;"True"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"true"&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;"False"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"false"&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;rendered&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;Compilation results cached by content hash is key for performance. Commit operations can trigger frequently—recompiling IL each time is unbearable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where Do Parameters Come From
&lt;/h3&gt;

&lt;p&gt;JSON metadata declares a dozen parameters: &lt;code&gt;projectPath&lt;/code&gt;, &lt;code&gt;needPush&lt;/code&gt;, &lt;code&gt;targetBranchMode&lt;/code&gt;, &lt;code&gt;gitProfileName&lt;/code&gt;, &lt;code&gt;gitProfileEmail&lt;/code&gt;, &lt;code&gt;signedOffByEnabled&lt;/code&gt;, &lt;code&gt;coAuthoredBy*&lt;/code&gt;, etc. These parameters are collected by the frontend "AI commit drawer," injected into backend via AutoTask channel, then routed by &lt;code&gt;FilePromptProvider&lt;/code&gt; to this template set via &lt;code&gt;PromptScenario.AutoComposeCommit&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three-State Handling for Branch Strategy
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;targetBranchMode&lt;/code&gt; determines whether the model touches branches before commit, a three-state:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;current&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Commit in place, don't touch branches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;new-custom&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create new branch from current branch using user-provided &lt;code&gt;targetBranchName&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ai-generated-new&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Model generates kebab-case branch name based on changes, add stable suffix if conflicts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The prompt explicitly says "don't switch to any other existing branch," preventing the model from arbitrarily switching to main branch to commit. This capability corresponds to the &lt;code&gt;auto-branch-switch-on-commit&lt;/code&gt; proposal. After all, once main branch is messed up, rolling back is a messy affair.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Complete Rendering Example
&lt;/h2&gt;

&lt;p&gt;Suppose the user selects in frontend: stay on current branch, need push, enable Signed-off-by, disable Co-Authored-By, Git identity is &lt;code&gt;newbe &amp;lt;newbe@newbe.pro&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then the &lt;code&gt;&amp;lt;git_profile&amp;gt;&lt;/code&gt; section renders to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;git_profile&amp;gt;&lt;/span&gt;
Use the following Git identity in all generated commits:
- Selected name: newbe
- Selected email: newbe@newbe.pro
...
- This run also requires Git standard sign-off trailer, so prefer using `git ... commit --author=... --signoff ...`
&lt;span class="nt"&gt;&amp;lt;/git_profile&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;requirements&amp;gt;&lt;/code&gt; keeps only the "Co-Authored-By disabled for this run" branch, and &lt;code&gt;&amp;lt;workflow&amp;gt;&lt;/code&gt; gives commands like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Note -c sets Committer, --author sets Author, --signoff adds DCO trailer&lt;/span&gt;
git &lt;span class="nt"&gt;-c&lt;/span&gt; user.name&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"newbe"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; user.email&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"newbe@newbe.pro"&lt;/span&gt; commit &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--author&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"newbe &amp;lt;newbe@newbe.pro&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;--signoff&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"type(scope): subject"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Engineering Practices for Template Maintenance
&lt;/h2&gt;

&lt;p&gt;HagiCode provides a full set of engineering safeguards for these &lt;code&gt;.hbs&lt;/code&gt; templates—not just writing them and calling it done.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;snapshot tests&lt;/strong&gt;. Test directory has &lt;code&gt;BuildMessage_enUS.verified.txt&lt;/code&gt;, &lt;code&gt;BuildMessage_zhCN.verified.txt&lt;/code&gt; and other verified snapshots—any rendering difference in templates is caught by tests. Change one character and you must update snapshots, preventing prompts from silently drifting.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;formatting scripts&lt;/strong&gt;. &lt;code&gt;cleanup-prompts.py --fix&lt;/code&gt; cleans trailing whitespace, collapses excess blank lines—CI check fails and blocks PR directly if not passing.&lt;/p&gt;

&lt;p&gt;Third, &lt;strong&gt;parameter validation&lt;/strong&gt;. Each scenario's required parameters, default values, types have dedicated test coverage—if template uses &lt;code&gt;{{newParam}}&lt;/code&gt; but JSON doesn't declare it, tests fail.&lt;/p&gt;

&lt;p&gt;Fourth, &lt;strong&gt;snapshot layering&lt;/strong&gt;: &lt;code&gt;Snapshots/Rendered/&lt;/code&gt; stores render results, &lt;code&gt;Snapshots/Scenarios/&lt;/code&gt; stores scenario metadata, ensuring consistency between templates, metadata, and render products.&lt;/p&gt;

&lt;p&gt;Here's a practical pitfall reminder. If you want to add new parameters or branches to this prompt system, four things must be done synchronously:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use &lt;code&gt;{{newParam}}&lt;/code&gt; in template (&lt;code&gt;.hbs&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Declare schema in &lt;code&gt;parameters&lt;/code&gt; array in metadata (&lt;code&gt;.json&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Update corresponding &lt;code&gt;.verified.txt&lt;/code&gt; in snapshot tests&lt;/li&gt;
&lt;li&gt;Frontend form generates input controls based on new JSON parameters and passes through API&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Missing any link either means empty parameters during rendering, red snapshot tests, or frontend unable to configure. This "four-way sync" constraint looks annoying, but for maintainability, it must be so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Prompts Are So "Verbose"
&lt;/h2&gt;

&lt;p&gt;Looking back at this prompt, you'll find it unusually verbose—identity, trailers, output format repeatedly emphasized. This is actually deliberate.&lt;/p&gt;

&lt;p&gt;Models in Agent mode are particularly prone to "acting on their own," so hard constraints must be scattered and repeatedly declared across &lt;code&gt;&amp;lt;requirements&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;workflow&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;final_instruction&amp;gt;&lt;/code&gt; to reduce probability of missed execution. This is like onboarding new people—say important things three times, not because they're slow, but because too many things compete for attention.&lt;/p&gt;

&lt;p&gt;In non-interactive mode (CI/CD, automation), models can't ask users questions, so the prompt explicitly states at the start "prohibit using AskUserQuestion, use defaults for missing info and record assumptions," ensuring it runs unattended.&lt;/p&gt;

&lt;p&gt;Once output contract loosens, backend parsing crashes—so &lt;code&gt;---&lt;/code&gt; separation rule is emphasized twice. Important things do indeed need to be said three times.&lt;/p&gt;

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

&lt;ul&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/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Conventional Commits specification: &lt;a href="https://www.conventionalcommits.org/" rel="noopener noreferrer"&gt;conventionalcommits.org&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Handlebars.Net: &lt;a href="https://github.com/Handlebars-Net/Handlebars.Net" rel="noopener noreferrer"&gt;github.com/Handlebars-Net/Handlebars.Net&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Git DCO (Developer Certificate of Origin): &lt;a href="https://developercertificate.org/" rel="noopener noreferrer"&gt;developercertificate.org&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Returning to the theme "Prompts for AI Commits in HagiCode: Design Rationale and Implementation Breakdown," what's worth repeatedly confirming isn't scattered techniques, but whether constraints, implementation boundaries, and engineering trade-offs are clearly seen.&lt;/p&gt;

&lt;p&gt;Once you distill the judgment basis in this article into stable checklist items, you can make reliable decisions faster when facing similar problems.&lt;/p&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-06-20-hagicode-ai-commit-prompt-design%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-20-hagicode-ai-commit-prompt-design%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>handlebars</category>
      <category>hagicode</category>
    </item>
    <item>
      <title>Integrating Reasonix 1.x with DeepSeek V4: ACP Model Selector Integration in Practice</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Fri, 19 Jun 2026 01:50:20 +0000</pubDate>
      <link>https://dev.to/newbe36524/integrating-reasonix-1x-with-deepseek-v4-acp-model-selector-integration-in-practice-19dp</link>
      <guid>https://dev.to/newbe36524/integrating-reasonix-1x-with-deepseek-v4-acp-model-selector-integration-in-practice-19dp</guid>
      <description>&lt;h1&gt;
  
  
  Integrating Reasonix 1.x with DeepSeek V4: ACP Model Selector Integration in Practice
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;This article discusses how to switch Reasonix 1.x, a local ACP CLI provider, to DeepSeek V4 in HagiCode. The focus isn't really on "getting it in," but rather on the semantic changes from Reasonix 0.x to 1.x—startup parameters were cut down to just one &lt;code&gt;-model&lt;/code&gt;, credentials and policies moved to &lt;code&gt;reasonix.toml&lt;/code&gt;. Let's walk through the pitfalls encountered and the verification path, bit by bit.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Recently someone asked a pretty specific question: how to integrate Reasonix 1.x version to use DeepSeek V4 in HagiCode.&lt;/p&gt;

&lt;p&gt;At first glance it looked like a configuration question, but digging into the code revealed it's actually a CLI semantic migration problem. Reasonix is a local ACP (Agent Communication Protocol) CLI within HagiCode's multi-Agent Provider system. Its position in HagiCode's three-tier architecture is clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HagiCode.Libs&lt;/strong&gt; — &lt;code&gt;ReasonixProvider&lt;/code&gt;, &lt;code&gt;ReasonixOptions&lt;/code&gt;, wrapping &lt;code&gt;reasonix acp&lt;/code&gt; process startup, ACP handshake, streaming notification mapping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;hagicode-core&lt;/strong&gt; — &lt;code&gt;ReasonixCliProvider&lt;/code&gt; thin adapter, &lt;code&gt;AIProviderType.ReasonixCli = 12&lt;/code&gt;, &lt;code&gt;ReasonixGrain&lt;/code&gt;, Hero parameter mapping, health monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;web&lt;/strong&gt; — OpenAPI types, visual mapping, Hero configuration forms, multilingual copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire integration chain is already implemented in the archived proposal &lt;code&gt;openspec/changes/archive/2026-06-06-integrate-reasonix-agent-provider&lt;/code&gt;. So the question isn't "how to get Reasonix into the system" anymore, but "once it's in, how to switch the model to DeepSeek V4".&lt;/p&gt;

&lt;p&gt;The key turning point is: Reasonix 1.x and 0.x have fundamentally different ACP bootstrap semantics. This change directly determines how you configure DeepSeek V4. After all, once semantics change, even if the surface looks similar, it's a different beast entirely.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'll leave that hanging: to untangle this complexity of multiple providers and multiple models, HagiCode adopted a "field preservation, semantic migration" design at the Reasonix adaptation layer. I'll explain exactly why this trade-off was chosen later.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;blockquote&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 an AI coding assistant project supporting multiple local/remote Agent Providers. Code is open sourced at &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode-org/site&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;h3&gt;
  
  
  1.x Cut Startup Parameters to Just One
&lt;/h3&gt;

&lt;p&gt;Look directly at &lt;code&gt;ReasonixProvider.BuildCommandArguments&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;internal&lt;/span&gt; &lt;span class="k"&gt;virtual&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;BuildCommandArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReasonixOptions&lt;/span&gt; &lt;span class="n"&gt;options&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;arguments&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;List&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="s"&gt;"acp"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="c1"&gt;// Reasonix 1.x reduced ACP bootstrap to a transport-scoped provider selector.&lt;/span&gt;
    &lt;span class="nf"&gt;AppendOption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arguments&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="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="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;argument&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;NormalizeExtraArguments&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;ExtraArguments&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;argument&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;arguments&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;That comment is the key: 1.x condensed ACP bootstrap to a "transport-scoped provider selector." In plain English—the only flag still meaningful at startup is &lt;code&gt;-model&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those legacy flags from the 0.x era are explicitly filtered out:&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;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;HashSet&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="n"&gt;FilteredBootstrapFlags&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;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&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;"-m"&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;"-dir"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--dir"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-effort"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--effort"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-budget"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--budget"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-transcript"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--transcript"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-mcp-prefix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--mcp-prefix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-yolo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"--yolo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"--dangerously-skip-permissions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"--no-proxy"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unit tests directly prove this. Pass in a bunch of legacy flags, the command line comes out clean, no errors, just silently dropped:&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="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShouldBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"acp"&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;"deepseek-v4-flash"&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ReasonixOptions Fields Still There, But Semantics Changed
&lt;/h3&gt;

&lt;p&gt;Here's an interesting design choice. Fields like &lt;code&gt;Effort&lt;/code&gt;, &lt;code&gt;BudgetUsd&lt;/code&gt;, &lt;code&gt;TranscriptPath&lt;/code&gt;, &lt;code&gt;EnableYolo&lt;/code&gt;, &lt;code&gt;McpServerSpecs&lt;/code&gt;, &lt;code&gt;McpPrefix&lt;/code&gt; in &lt;code&gt;ReasonixOptions&lt;/code&gt; are all preserved, but each comment honestly states "Reasonix 1.x ACP no longer accepts ... so this value is currently ignored".&lt;/p&gt;

&lt;p&gt;This is a classic &lt;strong&gt;field preservation, semantic migration&lt;/strong&gt; pattern: caller contracts aren't broken (0.x code still compiles, still passes values), but at runtime these values are silently dropped. Policy-type things (permissions, MCP plugins, proxy) are required to move to &lt;code&gt;reasonix.toml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To put it another way, it's like your original light switch is still on the wall, but the renovation guy changed the wiring. Now the switch is just decoration, the real lighting control moved to the smart home panel. The switch looks unchanged, pressing it doesn't error out, but the light just doesn't turn on.&lt;/p&gt;

&lt;p&gt;So the core action for integrating DeepSeek V4 is actually just one sentence: &lt;strong&gt;pass the model id through the &lt;code&gt;-model&lt;/code&gt; selector, configure credentials/endpoint in &lt;code&gt;reasonix.toml&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How DeepSeek V4 Gets In
&lt;/h3&gt;

&lt;p&gt;In HagiCode's tests and README, the DeepSeek series uses the standard pattern through the &lt;code&gt;Model&lt;/code&gt; field:&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;reasonixOptions&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;ReasonixOptions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;WorkingDirectory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/path/to/repo"&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="s"&gt;"deepseek-flash"&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="s"&gt;"reasonix-session-123"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tests repeatedly show &lt;code&gt;Model = "deepseek-v4-flash"&lt;/code&gt;, corresponding to the generated command line &lt;code&gt;reasonix acp -model deepseek-v4-flash&lt;/code&gt;. The specific model id (&lt;code&gt;deepseek-v4-flash&lt;/code&gt;, &lt;code&gt;deepseek-flash&lt;/code&gt;, etc.) should follow the Reasonix 1.x version you installed and the provider aliases registered in &lt;code&gt;reasonix.toml&lt;/code&gt;—after all, whether an alias is real or not, Reasonix knows best.&lt;/p&gt;

&lt;h3&gt;
  
  
  Working Directory and Session Recovery Go Through ACP, Not CLI Flags
&lt;/h3&gt;

&lt;p&gt;This is the second semantic change in 1.x, easy to get confused about. In 0.x you used &lt;code&gt;--dir&lt;/code&gt; to specify working directory, in 1.x it changed to using &lt;code&gt;session/new&lt;/code&gt; / &lt;code&gt;session/load&lt;/code&gt; within the ACP protocol:&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;sessionHandle&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;sessionClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartSessionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;workingDirectory&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;SessionId&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="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// Model selection completely determined by startup -model&lt;/span&gt;
    &lt;span class="n"&gt;startupCts&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the &lt;code&gt;model&lt;/code&gt; parameter in &lt;code&gt;StartSessionAsync&lt;/code&gt; passes &lt;code&gt;null&lt;/code&gt;—model selection is completely determined by &lt;code&gt;-model&lt;/code&gt; at startup, session level no longer overrides the model. &lt;code&gt;SessionId&lt;/code&gt; is still a provider-native continuity hint, used only to resume sessions.&lt;/p&gt;

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

&lt;p&gt;Putting the above analysis together into an executable path, let's walk through four steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install Reasonix CLI
&lt;/h3&gt;

&lt;p&gt;Reasonix is a locally installed, &lt;code&gt;IsPubliclyInstallable: false&lt;/code&gt; provider, can't be installed via npm publicly. First put the &lt;code&gt;reasonix&lt;/code&gt; executable in PATH. After installing, verify with HagiCode.Libs' built-in console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run Ping scenario, execute reasonix acp handshake and report version&lt;/span&gt;
dotnet run &lt;span class="nt"&gt;--project&lt;/span&gt; src/HagiCode.Libs.Reasonix.Console &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--test-provider&lt;/span&gt; reasonix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If handshake fails, it's usually one of two cases: either PATH didn't find &lt;code&gt;reasonix&lt;/code&gt;, or &lt;code&gt;reasonix.toml&lt;/code&gt; isn't configured. There aren't really any other reasons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Configure DeepSeek V4 Credentials in reasonix.toml
&lt;/h3&gt;

&lt;p&gt;1.x no longer accepts startup flags like &lt;code&gt;--api-key&lt;/code&gt;, &lt;code&gt;--base-url&lt;/code&gt;. Model provider endpoint, API key, proxy policies all need to be written to &lt;code&gt;reasonix.toml&lt;/code&gt;. Configuration roughly includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DeepSeek V4 API endpoint&lt;/li&gt;
&lt;li&gt;DeepSeek API key&lt;/li&gt;
&lt;li&gt;Aliases you want to expose to the &lt;code&gt;-model&lt;/code&gt; selector (like &lt;code&gt;deepseek-v4-flash&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Specific field names should follow the documentation of your installed Reasonix version. HagiCode's side only cares about passing through &lt;code&gt;-model deepseek-v4-flash&lt;/code&gt;, how this alias resolves to the actual model is Reasonix's business—responsibility boundaries are clearly drawn, don't cross lines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Configure HagiCode's ProviderConfiguration
&lt;/h3&gt;

&lt;p&gt;The resolution priority in backend &lt;code&gt;ReasonixCliProvider.ResolveModel&lt;/code&gt; is: request.Model takes priority, otherwise fall back to &lt;code&gt;_config.Model&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;private&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;ResolveModel&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="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;model&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;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="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_config&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="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;IsNullOrWhiteSpace&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="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;model&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So in &lt;code&gt;appsettings&lt;/code&gt; or runtime config, set the provider's Model to DeepSeek V4's alias:&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;"AIProvider"&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;"ReasonixCli"&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;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ReasonixCli"&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;"deepseek-v4-flash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Settings"&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;p&gt;Here's a trap that's easy to step in: &lt;code&gt;Settings&lt;/code&gt; can only hold keys within the whitelist:&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;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;IReadOnlyList&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="n"&gt;SupportedSettingKeys&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"effort"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"budgetUsd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"transcriptPath"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"enableYolo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"startupTimeoutMs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"reasoning"&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ValidateConfigurationOverrides&lt;/code&gt; will directly reject keys outside the whitelist. And most of these keys are ignored in 1.x (corresponding to those ignored fields in &lt;code&gt;ReasonixOptions&lt;/code&gt;), so &lt;strong&gt;never stuff DeepSeek credentials into Settings&lt;/strong&gt;, that's not where they belong, credentials go to &lt;code&gt;reasonix.toml&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Use Console for End-to-End Verification
&lt;/h3&gt;

&lt;p&gt;After configuration, run the full suite with Reasonix's dedicated console, explicitly specifying the model as DeepSeek V4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Default suite: four scenarios - Ping / Simple Prompt / Complex Prompt / Session Resume&lt;/span&gt;
dotnet run &lt;span class="nt"&gt;--project&lt;/span&gt; src/HagiCode.Libs.Reasonix.Console &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--test-provider-full&lt;/span&gt; &lt;span class="nt"&gt;--model&lt;/span&gt; deepseek-v4-flash &lt;span class="nt"&gt;--repo&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all four scenarios pass green, the model selector, ACP handshake, streaming notifications, session recovery—the entire chain is connected. Green means peace of mind.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  How to Fill the Hero Configuration Form in Frontend
&lt;/h3&gt;

&lt;p&gt;If you go through HagiCode's Hero career UI instead of directly modifying appsettings, after selecting Reasonix in &lt;code&gt;HeroCliEquipmentForm&lt;/code&gt;, the form fields are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;binary&lt;/strong&gt;: default &lt;code&gt;reasonix&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;model&lt;/strong&gt;: fill &lt;code&gt;deepseek-v4-flash&lt;/code&gt; (key field for switching to DeepSeek V4)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;effort&lt;/strong&gt;: none / low / medium / high (ignored in 1.x, but UI still keeps it)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;budgetUsd&lt;/strong&gt;: number (ignored in 1.x)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;transcriptPath&lt;/strong&gt;: text (ignored in 1.x)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;enableYolo&lt;/strong&gt;: boolean (ignored in 1.x, permissions go to toml)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;arguments&lt;/strong&gt;: extra parameters passed through to ACP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;startupTimeoutMs&lt;/strong&gt;: default 15000&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Actually, only the &lt;code&gt;model&lt;/code&gt; field affects DeepSeek V4 behavior, the rest are just decoration under 1.x. This is also HagiCode's "field preservation, semantic migration" design reflected in the UI—the form doesn't break old user habits, but the fields that actually take effect are converged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Session Binding and Recovery
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ReasonixCliProvider&lt;/code&gt; uses &lt;code&gt;ConcurrentDictionary&amp;lt;string, string&amp;gt;&lt;/code&gt; to maintain session bindings, binding key is calculated from sessionId, working directory, executable path, and model:&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;bindingKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NormalizedAcpCliAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BuildBindingKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;effectiveRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CessionId&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;WorkingDirectory&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;ExecutablePath&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means if you switch models mid-session in the same session, the binding key changes and it's treated as a new session. So &lt;strong&gt;after integrating DeepSeek V4, keep the model alias stable throughout the session lifecycle&lt;/strong&gt;, otherwise resume will break. I learned this the hard way through actual testing—blood and tears lesson, still remember the taste.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitoring and Degradation
&lt;/h3&gt;

&lt;p&gt;Reasonix uses the &lt;code&gt;Provider&lt;/code&gt; strategy (not &lt;code&gt;Grain&lt;/code&gt; strategy) in &lt;code&gt;AgentCliMonitoringRegistry&lt;/code&gt;, since it might not be installed:&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;new&lt;/span&gt; &lt;span class="n"&gt;AgentCliMonitoringDescriptor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;CliId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"reasonix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DisplayName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Reasonix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ProviderType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AIProviderType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReasonixCli&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="n"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// ping-based, via PATH discovery&lt;/span&gt;
    &lt;span class="n"&gt;ExecutableCandidates&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"reasonix"&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;Frontend health checks show whether Reasonix is available. If &lt;code&gt;reasonix&lt;/code&gt; isn't in PATH, the UI gracefully degrades to "unavailable"—this logic is built in, no need to worry about it yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Several Practical Points to Note
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Authenticity of Model Aliases&lt;/strong&gt;: &lt;code&gt;deepseek-v4-flash&lt;/code&gt; must be a real alias registered in &lt;code&gt;reasonix.toml&lt;/code&gt;, otherwise ACP handshake passes but sending prompts still fails. Verify with console first before going to Hero, don't cut corners.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't Use &lt;code&gt;arguments&lt;/code&gt; to Pass Legacy Flags&lt;/strong&gt;: &lt;code&gt;NormalizeExtraArguments&lt;/code&gt; filters out &lt;code&gt;--effort&lt;/code&gt;, &lt;code&gt;--budget&lt;/code&gt;, etc., passing them is futile, wasted effort.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credentials Only in toml&lt;/strong&gt;: API key, endpoint, proxy, MCP plugins all go in &lt;code&gt;reasonix.toml&lt;/code&gt;, HagiCode's side Settings whitelist doesn't have these fields at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;startupTimeoutMs is Adjustable&lt;/strong&gt;: If DeepSeek V4 cold start is slow, raise &lt;code&gt;startupTimeoutMs&lt;/code&gt; from default 15000, this field is recognized in 1.x.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Economic System Maps to Claude Bucket&lt;/strong&gt;: Frontend &lt;code&gt;resolveEconomicSystemByExecutorType&lt;/code&gt; maps Reasonix to the &lt;code&gt;'claude'&lt;/code&gt; bucket, purely for display, doesn't affect billing.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A Minimal Verification Path
&lt;/h3&gt;

&lt;p&gt;If you just want to confirm DeepSeek V4 works quickly without touching Hero UI:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install reasonix, configure &lt;code&gt;reasonix.toml&lt;/code&gt; (DeepSeek endpoint + key + alias)&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;ReasonixCli.Model = "deepseek-v4-flash"&lt;/code&gt; in &lt;code&gt;appsettings&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;dotnet run --project src/HagiCode.Libs.Reasonix.Console -- --test-provider-full --model deepseek-v4-flash&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;All four scenarios green, integration complete&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Returning to the original question—"how to integrate Reasonix 1.x to use DeepSeek v4".&lt;/p&gt;

&lt;p&gt;The answer is actually just one sentence: &lt;strong&gt;pass the model alias through the &lt;code&gt;-model&lt;/code&gt; selector, configure credentials and policies in &lt;code&gt;reasonix.toml&lt;/code&gt;, don't count on CLI flags&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Behind that one sentence is a pretty clean semantic convergence by Reasonix 1.x: startup parameters cut to just &lt;code&gt;-model&lt;/code&gt;, working directory and session recovery moved into ACP protocol, policies all sunk into toml. HagiCode's adaptation layer didn't fight this change head-on, but chose the gentle "field preservation, semantic migration" route—old code still compiles, still passes values, silently ignored at runtime, converging effective switches to just &lt;code&gt;-model&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The benefit of this trade-off is smooth migration, the cost is documentation needs to be clear—which is why this article exists. Just remember three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Models go through &lt;code&gt;-model&lt;/code&gt;&lt;/strong&gt;, DeepSeek V4 is &lt;code&gt;-model deepseek-v4-flash&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credentials go to toml&lt;/strong&gt;, don't stuff them into Settings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't switch models mid-session&lt;/strong&gt;, binding key changes, resume breaks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;HagiCode chose to design the Reasonix adaptation layer this way essentially because it needs to accommodate multiple providers, multiple model versions, multiple deployment forms. This complexity of multiple languages, multiple platforms is exactly why we repeatedly polish the provider adaptation strategy in HagiCode.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Reasonix Provider implementation: &lt;code&gt;repos/Hagicode.Libs/src/HagiCode.Libs.Providers/Reasonix/ReasonixProvider.cs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Reasonix Options field semantics: &lt;code&gt;repos/Hagicode.Libs/src/HagiCode.Libs.Providers/Reasonix/ReasonixOptions.cs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Backend thin adapter: &lt;code&gt;repos/hagicode-core/src/PCode.ClaudeHelper/AI/Providers/ReasonixCliProvider.cs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Integration proposal archive: &lt;code&gt;openspec/changes/archive/2026-06-06-integrate-reasonix-agent-provider&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Backend spec: &lt;code&gt;openspec/specs/reasonix-backend-integration/spec.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Unit tests (including deepseek-v4-flash cases): &lt;code&gt;repos/Hagicode.Libs/tests/HagiCode.Libs.Providers.Tests/ReasonixProviderTests.cs&lt;/code&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;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Around "Integrating Reasonix 1.x with DeepSeek V4: ACP Model Selector Integration in Practice," a more solid approach is to first gradually get key configurations, dependency boundaries, and implementation paths working, then fill in optimization details.&lt;/p&gt;

&lt;p&gt;When goals, steps, and acceptance criteria are clear, this type of solution usually flows more smoothly into actual delivery.&lt;/p&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-06-19-reasonix-1x-deepseek-v4-acp-integration%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-19-reasonix-1x-deepseek-v4-acp-integration%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>reasonix</category>
      <category>deepseek</category>
      <category>acp</category>
      <category>hagicode</category>
    </item>
    <item>
      <title>Pi Agent Integration: Message Parsing, Retry, and Cancellation</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Fri, 19 Jun 2026 01:12:00 +0000</pubDate>
      <link>https://dev.to/newbe36524/pi-agent-integration-message-parsing-retry-and-cancellation-nl0</link>
      <guid>https://dev.to/newbe36524/pi-agent-integration-message-parsing-retry-and-cancellation-nl0</guid>
      <description>&lt;h1&gt;
  
  
  Pi Agent Integration: Message Parsing, Retry, and Cancellation
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;When integrating a CLI-based AI agent, you can't avoid three things: how to translate its private event stream into stable messages, who's responsible for retry after failures, and how to cleanly stop the process when users click cancel. These three things essentially boil down to "clarifying responsibilities"—it's simple in theory, but you only realize how deep the water is when you actually do it.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Recently, I've been working on an AI coding assistant project, and one of the agents to integrate is &lt;a href="https://github.com/earendil-works/pi-coding-agent" rel="noopener noreferrer"&gt;pi&lt;/a&gt;. It's a TUI/CLI coding agent that outputs JSON events line by line to stdout when running. Sounds simple—spawn the process, read output, parse it—but when you actually start, you realize "integrating an agent CLI" is completely different from "integrating a regular CLI."&lt;/p&gt;

&lt;p&gt;With a regular CLI, you read stdout, get an exit code, and that's it. But agent CLIs have three particularly headache-inducing characteristics:&lt;/p&gt;

&lt;p&gt;First, its event stream is a &lt;strong&gt;private protocol&lt;/strong&gt;. &lt;code&gt;turn_start&lt;/code&gt;, &lt;code&gt;session&lt;/code&gt;, &lt;code&gt;message_update&lt;/code&gt;, &lt;code&gt;message_end&lt;/code&gt;, &lt;code&gt;turn_end&lt;/code&gt;, &lt;code&gt;agent_end&lt;/code&gt;—these are defined by pi itself, not any industry standard. Every upper layer that wants to consume it has to handle it separately, effectively leaking pi's internal details everywhere. It's like looking at someone from a distance—you think you see them clearly, but you're only seeing the side they want to show you.&lt;/p&gt;

&lt;p&gt;Second, its failure semantics are &lt;strong&gt;particularly ambiguous&lt;/strong&gt;. The agent might encounter network jitter, model rate limiting, or process crashes while running. Should it retry? Where to retry? Will retry mess up the session state that's already half-output? This is an architectural decision, not something you can solve by casually writing a &lt;code&gt;for&lt;/code&gt; loop.&lt;/p&gt;

&lt;p&gt;Third, it's &lt;strong&gt;long-running and interruptible&lt;/strong&gt;. A single turn might run for tens of seconds or even minutes, and users might want to cancel at any time. When canceled, the process can't become an orphan, tool calls can't be left half-finished, and already output content can't be lost. The water here is much deeper than imagined.&lt;/p&gt;

&lt;p&gt;To solve these pain points, we spent some time straightening out the integration path. I'll get into specifics later, but here's a spoiler: the real difficulty isn't in "spawning the process," but in "clarifying responsibilities."&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;The solution shared in this article comes from the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project—an AI coding assistant that supports multiple models and multiple agent CLI backends. GitHub repository: &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode-org/site&lt;/a&gt;, feel free to star it. All the code and all the pitfalls mentioned below are actually running in this project. Writing this out is just leaving myself a memory.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Overall Layering
&lt;/h2&gt;

&lt;p&gt;HagiCode splits AI capability integration into two layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The bottom layer is &lt;code&gt;Hagicode.Libs&lt;/code&gt;, providing reusable provider primitives &lt;code&gt;ICliProvider&amp;lt;TOptions&amp;gt;&lt;/code&gt;, specifically responsible for "spawning a CLI agent and normalizing its output into a shared message stream."&lt;/li&gt;
&lt;li&gt;The upper layer is &lt;code&gt;hagicode-core&lt;/code&gt;, providing project-level thin adapters &lt;code&gt;IAIProvider&lt;/code&gt;, responsible for "translating business requests into provider parameters, consuming shared message streams, and exposing unified streaming chunks externally."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pi's integration follows this path. The bottom layer &lt;code&gt;PiProvider&lt;/code&gt; spawns the pi process, reads the JSON event stream, and normalizes it into shared messages; the upper layer &lt;code&gt;PiCliProvider&lt;/code&gt; translates &lt;code&gt;AIRequest&lt;/code&gt; into &lt;code&gt;PiOptions&lt;/code&gt;, consumes &lt;code&gt;CliMessage&lt;/code&gt;, and outputs &lt;code&gt;AIStreamingChunk&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These three things—message parsing, retry, cancellation—fall into three different places: &lt;code&gt;PiJsonEventMapper&lt;/code&gt;, a seemingly strange archiving proposal, and &lt;code&gt;CliProcessManager&lt;/code&gt;. Let's talk about each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Message Parsing: Converting Pi's Private Events to Shared Messages
&lt;/h2&gt;

&lt;p&gt;pi outputs JSON events line by line under &lt;code&gt;--mode json --print&lt;/code&gt;. This event set is private to pi and must not be leaked directly to upper layers, otherwise every consumer would couple to pi's internal details, and the entire project would follow pi's upgrades when its event structure changes. This kind of leakage is similar to writing your thoughts on your face—others find it tiring to look at, and you're not necessarily comfortable either.&lt;/p&gt;

&lt;p&gt;We use &lt;code&gt;PiJsonEventMapper&lt;/code&gt; as a translation layer to normalize pi's events into shared &lt;code&gt;CliMessage&lt;/code&gt;. &lt;code&gt;CliMessage&lt;/code&gt; is defined in &lt;code&gt;HagiCode.Libs.Core/Transport/CliMessage.cs&lt;/code&gt; with a very simple structure—just a &lt;code&gt;(Type, Content)&lt;/code&gt; record. The mapping relationship is roughly as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;pi event&lt;/th&gt;
&lt;th&gt;shared message&lt;/th&gt;
&lt;th&gt;purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;session&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;session.started&lt;/code&gt; / &lt;code&gt;session.resumed&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;session lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;message_update&lt;/code&gt; (text type)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;assistant&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;streaming body incremental&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;message_update&lt;/code&gt; (thinking type)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;assistant.thought&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;thought chain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;message_update&lt;/code&gt; (tool type)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tool.call&lt;/code&gt; / &lt;code&gt;tool.update&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;tool call initiation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;message_end&lt;/code&gt; / &lt;code&gt;turn_end&lt;/code&gt; (toolResult)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tool.completed&lt;/code&gt; / &lt;code&gt;tool.failed&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;tool result&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;turn_end&lt;/code&gt; / &lt;code&gt;agent_end&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terminal.completed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;current turn end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;non-zero exit / parse failure&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terminal.failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;terminal state failure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table is just a quick reference, but it contains two key techniques that were figured out after stumbling into pitfalls—worth expanding on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technique One: Converting Cumulative Snapshot to Delta
&lt;/h3&gt;

&lt;p&gt;This is the easiest place to crash. pi's &lt;code&gt;message_update&lt;/code&gt; event doesn't send increments, but &lt;strong&gt;cumulative full text&lt;/strong&gt;—every time a token comes, it resends the "complete text so far."&lt;/p&gt;

&lt;p&gt;If you forward received content directly to the frontend, users will see content repeated: the first is "you", the second is "hello", the third is "hello,", the fourth is "hello, wor"... The frontend will think these are four independent outputs. Repetition is fresh the first time you see it, but boring the tenth time.&lt;/p&gt;

&lt;p&gt;The solution is prefix comparison to calculate the true delta:&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;// Key: pi sends cumulative snapshot, not increment&lt;/span&gt;
&lt;span class="c1"&gt;// Use prefix comparison to extract the delta, otherwise frontend sees repeated content&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;text&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="n"&gt;_lastAssistantTextSnapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ordinal&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;delta&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;_lastAssistantTextSnapshot&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="n"&gt;_lastAssistantTextSnapshot&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;delta&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;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;delta&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;There's another hidden pitfall here: &lt;strong&gt;cross-turn prefix replay&lt;/strong&gt;. After tool calls end and the assistant continues speaking, pi will replay that previous text from the beginning again. If you only keep a global snapshot, you'll treat the replayed content as increments, causing repetition after tool calls. &lt;code&gt;PiProviderTests&lt;/code&gt; has a dedicated test case &lt;code&gt;ExecuteAsync_deduplicates_replayed_assistant_prefix_after_tool_turns&lt;/code&gt; covering this scenario. In other words, snapshots before and after tool calls need aligned processing, not independent handling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technique Two: Buffer Thinking Until Turn End
&lt;/h3&gt;

&lt;p&gt;Thought chains (thinking) can't be output as soon as each token is received. pi stuffs a bunch of thinking fragments in the middle of tool calls. If forwarded in real time, the stream order becomes a mess—一会儿是 assistant 正文，一会儿是思考碎片，一会儿又是 tool.call。Does this make sense? It doesn't really, just adding confusion.&lt;/p&gt;

&lt;p&gt;Our approach: when receiving thinking events, first put them in &lt;code&gt;BufferThinkingSnapshot&lt;/code&gt; for temporary storage, and wait until &lt;code&gt;message_end&lt;/code&gt; or &lt;code&gt;turn_end&lt;/code&gt; with &lt;code&gt;stopReason != "toolUse"&lt;/code&gt;, then uniformly &lt;code&gt;DrainBufferedThinkingMessages&lt;/code&gt;. This way, thinking fragments in the middle of tool calls won't pollute the main stream, and the complete thought process is given all at once when the turn ends.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fault Tolerance: Bad Lines Can't Crash the Stream
&lt;/h3&gt;

&lt;p&gt;Agent CLI isn't the ideal system from textbooks. It occasionally spits out a non-JSON line, or a JSON without a &lt;code&gt;type&lt;/code&gt; field. If you throw an exception here, the entire stream dies and users see nothing. The real world isn't always perfect—who can guarantee every line is well-behaved?&lt;/p&gt;

&lt;p&gt;Our strategy: any line that fails to parse doesn't interrupt the stream, but is collected into &lt;code&gt;_invalidOutputLines&lt;/code&gt;. After the process ends, in &lt;code&gt;Complete()&lt;/code&gt;, these "bad lines" are spliced into the diagnostic text of &lt;code&gt;terminal.failed&lt;/code&gt;. This way, when users see errors, they can directly see what garbage pi actually output, not a dry "parse error."&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry: If Provider Layer Doesn't Do It, Who Does?
&lt;/h2&gt;

&lt;p&gt;This is the easiest pitfall in the entire integration. Intuitively "integrating a CLI should include retry," but HagiCode in an archiving proposal &lt;strong&gt;actively removed&lt;/strong&gt; all automatic retry from the provider layer. The proposal is called &lt;code&gt;remove-provider-auto-retry-support&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why No Automatic Retry
&lt;/h3&gt;

&lt;p&gt;The proposal background is written very bluntly. Retry logic was originally scattered in two places: one copy in &lt;code&gt;Hagicode.Libs&lt;/code&gt; (OpenCode-style fresh-runtime replay), another in &lt;code&gt;hagicode-core&lt;/code&gt; (&lt;code&gt;ProviderErrorAutoRetryCoordinator&lt;/code&gt;). Both sides did their own thing, making "whether to retry" a hidden implicit behavior inside the provider, secretly changing failure timing, session continuation method, and chat state flow.&lt;/p&gt;

&lt;p&gt;Think about it and your head hurts: user sends a message, provider internally retries three times itself, first two fail, third succeeds. Upper layer has no idea what happened in the middle, session state, token counting, UI progress all don't match. This kind of implicit behavior is chronic poison in architecture.&lt;/p&gt;

&lt;p&gt;So the boundary was converged into one sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;provider converges to single-attempt semantics, caller needs to treat non-retry state as normal single-execution result.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What Does This Mean for PiProvider
&lt;/h3&gt;

&lt;p&gt;In code, it's three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PiOptions&lt;/code&gt; has &lt;strong&gt;no retry-related fields&lt;/strong&gt;—no &lt;code&gt;maxAttempts&lt;/code&gt;, no &lt;code&gt;retryDelay&lt;/code&gt;, no &lt;code&gt;retryClassifier&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ExecuteAsync&lt;/code&gt; ends after one pi process run completes, failures directly give &lt;code&gt;terminal.failed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Classifiers previously serving automatic retry (&lt;code&gt;ClaudeCodeRetryableTerminalFailureClassifier&lt;/code&gt;, &lt;code&gt;CodexRetryableTerminalFailureClassifier&lt;/code&gt;, etc.)—if they purely served automatic retry—are all removed from the active path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But please note, &lt;strong&gt;retry capability hasn't disappeared, just moved up&lt;/strong&gt;. The proposal explicitly writes "leaving a stable boundary for higher layers to uniformly take over retry later." The DTO for &lt;code&gt;providerErrorAutoRetry&lt;/code&gt; configuration item, normalization, serialization, frontend settings page round-trip are all preserved, it just no longer drives provider execution. Some things aren't really unwanted, just kept in a different way.&lt;/p&gt;

&lt;h3&gt;
  
  
  What If You Want to Retry
&lt;/h3&gt;

&lt;p&gt;If you want to add retry on top of pi, the correct approach is to do it at the caller of &lt;code&gt;PiCliProvider&lt;/code&gt;—for example, your session orchestration layer (in HagiCode it's Orleans's &lt;code&gt;SessionGrain&lt;/code&gt;, frontend might be chat orchestration layer). After getting &lt;code&gt;terminal.failed&lt;/code&gt;, judge yourself whether it's retryable, decide delay and count yourself, then send &lt;code&gt;ExecuteAsync&lt;/code&gt; again.&lt;/p&gt;

&lt;p&gt;A minimal viable pattern looks 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="c1"&gt;// Retry logic at caller, don't stuff back into PiProvider&lt;/span&gt;
&lt;span class="c1"&gt;// Otherwise breaks the "single-attempt" boundary just established by provider&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;AIResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ExecuteWithRetryAsync&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;req&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;maxAttempts&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;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;1&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;response&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;provider&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;req&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;// Return on success or reaching limit&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;FinishReason&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;FinishReason&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unknown&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;maxAttempts&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;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Only retry retryable terminal failures (network, 5xx, process crash)&lt;/span&gt;
        &lt;span class="c1"&gt;// model rejected, auth failure这类重试也无意义，别重试&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&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="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Pow&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="n"&gt;attempt&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The classification logic for judging "retryable" is no longer in the provider, caller defines it themselves. &lt;code&gt;providerErrorAutoRetry&lt;/code&gt; configuration (maxAttempts, retryDelay, enabled) can still be read from the frontend settings page, but what actually drives retry is your orchestration layer, not &lt;code&gt;PiProvider&lt;/code&gt;. Repeat this three times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cancellation: Token Pass-through + Three-stage Shutdown
&lt;/h2&gt;

&lt;p&gt;For cancellation, PiProvider almost doesn't implement anything itself, fully delegating to &lt;code&gt;CliProcessManager&lt;/code&gt;. PiProvider only handles two things: passing &lt;code&gt;CancellationToken&lt;/code&gt; down, and cleanup on exception.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full-chain Pass-through
&lt;/h3&gt;

&lt;p&gt;The chain looks like this, passing all the way down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;调用方 CancellationToken
  → PiCliProvider.StreamCoreAsync(cancellationToken)
  → PiProvider.ExecuteProcessAsync([EnumeratorCancellation] cancellationToken)
  → ReadLineAsync(cancellationToken) / WaitForExitAsync(cancellationToken)
  → 异常时 _processManager.StopAsync(handle, CancellationToken.None)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the last line: cleanup uses &lt;code&gt;CancellationToken.None&lt;/code&gt;, not the token the user passed in. This is a detail, but extremely important.&lt;/p&gt;

&lt;p&gt;The reason is: the user's token &lt;strong&gt;is already canceled&lt;/strong&gt;. If you use this already-canceled token for cleanup, the cleanup task will be canceled immediately, and the process becomes an orphan—pi still running in the background, no one collecting it, CPU and memory occupied for nothing. So cleanup must use &lt;code&gt;CancellationToken.None&lt;/code&gt;, ensuring cleanup actions definitely complete. It's like with people—some things need proper cleanup after they completely stop, otherwise it's just leaving a mess.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three-stage Progressive Shutdown
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CliProcessManager.StopProcessAsync&lt;/code&gt; is a three-stage progressive shutdown process, with time constants defined at the top of the file:&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;// 优雅停止的耐心：先给进程自己收尾的时间&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;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;GracefulStopTimeout&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;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// 强制 kill 后等待进程真正退出的耐心&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;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;StopWaitTimeout&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;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three stages progress like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Interrupt signal&lt;/strong&gt;. &lt;code&gt;TryInterruptAsync&lt;/code&gt; first writes a &lt;code&gt;\u0003&lt;/code&gt; (Ctrl+C character) to stdin, and on Unix additionally does &lt;code&gt;kill -INT &amp;lt;pid&amp;gt;&lt;/code&gt;. This step is to let pi gracefully end itself—it can perceive the interrupt and wrap up what it's writing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful wait&lt;/strong&gt;. Wait at most 2 seconds to see if the process exits on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Force kill&lt;/strong&gt;. If it hasn't exited, directly &lt;code&gt;Process.Kill(entireProcessTree: true)&lt;/code&gt; to kill the entire process tree together, then wait at most 5 seconds to confirm it's really dead.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why &lt;code&gt;entireProcessTree: true&lt;/code&gt;? Because pi spawns child processes when running tools—for example, local model processes routed by provider, bash subprocesses running. Killing only the parent process leaves child processes as orphans continuing to run. Killing the whole tree together is clean.&lt;/p&gt;

&lt;p&gt;Under Windows there's no SIGINT, can only rely on Ctrl+C character, so cross-platform behavior will differ, keep this in mind.&lt;/p&gt;

&lt;h3&gt;
  
  
  PiProvider's Exception Cleanup
&lt;/h3&gt;

&lt;p&gt;PiProvider's &lt;code&gt;ExecuteProcessAsync&lt;/code&gt; when &lt;code&gt;ReadLineAsync&lt;/code&gt; throws an exception, uses &lt;code&gt;ExceptionDispatchInfo.Capture&lt;/code&gt; to temporarily store the exception, jumps out of the loop to call &lt;code&gt;StopAsync&lt;/code&gt; to clean up the process, then &lt;code&gt;pendingException.Throw()&lt;/code&gt; rethrows the original exception to the upper layer.&lt;/p&gt;

&lt;p&gt;Why store then throw? Because if you throw directly, the process hasn't been recycled yet and becomes an orphan; if you throw before &lt;code&gt;StopAsync&lt;/code&gt;, cleanup logic doesn't run at all. Store it temporarily, first guarantee the process is definitely recycled, then preserve the original &lt;code&gt;OperationCanceledException&lt;/code&gt; semantics completely for the caller—caller gets this exception and can judge "oh, user actively canceled" not "something went wrong."&lt;/p&gt;

&lt;h2&gt;
  
  
  Unified Contract for Startup Failures
&lt;/h2&gt;

&lt;p&gt;There's another detail worth mentioning separately. Process startup failure—for example, pi executable doesn't exist, wrong permissions—PiProvider &lt;strong&gt;doesn't throw exceptions&lt;/strong&gt;, but synthesizes a &lt;code&gt;terminal.failed&lt;/code&gt; message, then &lt;code&gt;yield break&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why do this? Because if you throw exceptions, upper layer consumers have to handle two completely different semantics: one is "normal messages during streaming consumption," the other is "exceptions thrown before streaming starts." This makes the consumer's &lt;code&gt;await foreach&lt;/code&gt; particularly hard to write.&lt;/p&gt;

&lt;p&gt;After unifying to "always give you messages first, then end the stream," consumer logic is consistent: getting &lt;code&gt;terminal.failed&lt;/code&gt; counts as failure, getting &lt;code&gt;terminal.completed&lt;/code&gt; counts as success, no need for try/catch branching. This is a small but important design decision, stabilizing the contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice: Correct Way to Consume Streams
&lt;/h2&gt;

&lt;p&gt;Referencing &lt;code&gt;PiScenarioMessageReader&lt;/code&gt; (libs console test scenario) and &lt;code&gt;PiCliProvider.StreamCoreAsync&lt;/code&gt; (core thin adapter) in HagiCode, consumers look 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;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;provider&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;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="c1"&gt;// 1. 短路处理失败，别再处理后续消息&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;NormalizedAcpCliAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetFailureMessage&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="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;failure&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="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;AIStreamingChunk&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;StreamingChunkType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&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="n"&gt;failure&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="c1"&gt;// stream ends after terminal.failed&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. assistant 文本是 cumulative snapshot，自己再做一次增量计算&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;message&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="s"&gt;"assistant"&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;TryGetText&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="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;text&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;delta&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ReconcileSnapshot&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="c1"&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;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&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;Chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. terminal.completed 是唯一可靠的"结束"信号&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;message&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="s"&gt;"terminal.completed"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Common Pitfalls Quick Reference
&lt;/h3&gt;

&lt;p&gt;Organizing all the pitfalls encountered along the way into a table, for future reference:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;处理&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;前端看到 assistant 文本重复&lt;/td&gt;
&lt;td&gt;没做 cumulative 转 delta&lt;/td&gt;
&lt;td&gt;用 &lt;code&gt;ReconcileAssistantTextSnapshot&lt;/code&gt; 做前缀比对&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;取消后进程还在跑&lt;/td&gt;
&lt;td&gt;清理用了已经取消的 token&lt;/td&gt;
&lt;td&gt;改用 &lt;code&gt;CancellationToken.None&lt;/code&gt; 做清理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;重试不生效&lt;/td&gt;
&lt;td&gt;把重试写进了 PiProvider，但 provider 是单次尝试语义&lt;/td&gt;
&lt;td&gt;上移到调用方编排层&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pi 报错信息丢失&lt;/td&gt;
&lt;td&gt;没读 &lt;code&gt;terminal.failed&lt;/code&gt; 的诊断字段&lt;/td&gt;
&lt;td&gt;完整透传 &lt;code&gt;text&lt;/code&gt; / &lt;code&gt;invalid_output_lines&lt;/code&gt; / &lt;code&gt;stderr&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;工具调用中途收到思考碎片&lt;/td&gt;
&lt;td&gt;直接转发了 thinking 事件&lt;/td&gt;
&lt;td&gt;缓冲到 turn 结束再 &lt;code&gt;DrainBufferedThinkingMessages&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  How to Verify
&lt;/h3&gt;

&lt;p&gt;The libs layer uses &lt;code&gt;StubCliProcessManager&lt;/code&gt; to mock processes, with unit tests covering pure logic like parameter construction, event normalization, incremental deduplication, failure pass-through. The real CLI path uses &lt;code&gt;HAGICODE_REAL_CLI_TESTS&lt;/code&gt; environment variable to opt-in, running trip scenarios with real models. The core layer's &lt;code&gt;PiCliProviderTests&lt;/code&gt; verifies the thin adapter's &lt;code&gt;AIStreamingChunk&lt;/code&gt; projection and session binding.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 在 Hagicode.Libs 仓库跑 Pi 相关单测&lt;/span&gt;
dotnet &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s2"&gt;"FullyQualifiedName~PiProviderTests"&lt;/span&gt;

&lt;span class="c"&gt;# 跑真实 CLI 集成测试（需要本地装好 pi）&lt;/span&gt;
&lt;span class="nv"&gt;HAGICODE_REAL_CLI_TESTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 dotnet &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s2"&gt;"FullyQualifiedName~PiProviderTests.RealCli"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Putting these three things together, the mental model for integrating pi actually boils down to one sentence: &lt;strong&gt;let each layer do only its own thing.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Message parsing is handed to &lt;code&gt;PiJsonEventMapper&lt;/code&gt;: private events are normalized into shared &lt;code&gt;CliMessage&lt;/code&gt;, cumulative snapshots converted to deltas, thinking buffered until turn end.&lt;/li&gt;
&lt;li&gt;Retry is handed to the caller: provider does single attempts, whoever wants retry does it at the upper layer themselves, configuration is preserved but no longer drives provider.&lt;/li&gt;
&lt;li&gt;Cancellation is handed to &lt;code&gt;CliProcessManager&lt;/code&gt;: &lt;code&gt;CancellationToken&lt;/code&gt; is passed through the full chain, cleanup uses &lt;code&gt;CancellationToken.None&lt;/code&gt;, three-stage progressive shutdown (interrupt signal → graceful wait → force kill entire process tree).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After these boundaries are clearly drawn, integrating a new agent CLI almost becomes pipeline work—you just need to write a new &lt;code&gt;XxxProvider&lt;/code&gt; and &lt;code&gt;XxxJsonEventMapper&lt;/code&gt;, and cross-cutting logic like retry, cancellation, message contracts, error handling are all reused. This is also the fundamental reason why HagiCode can simultaneously support multiple agent CLI backends (claude code, codex, pi, gemini cli, etc.) without getting messy.&lt;/p&gt;

&lt;p&gt;Let me say that most important boundary one more time: &lt;strong&gt;don't add retry at the provider layer&lt;/strong&gt;. Once you understand this, integrating agent CLI is more than halfway done...&lt;/p&gt;

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

&lt;p&gt;Returning to the theme "Pi Agent Integration: Message Parsing, Retry, and Cancellation," what's really worth repeatedly confirming isn't scattered techniques, but whether constraints, implementation boundaries, and engineering trade-offs have been clearly seen.&lt;/p&gt;

&lt;p&gt;As long as the judgment bases in this article are settled into stable checklist items, you can make reliable decisions faster when facing similar problems in the future.&lt;/p&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-06-19-pi-agent-integration-message-parsing-retry-cancel%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-19-pi-agent-integration-message-parsing-retry-cancel%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>hagicode</category>
      <category>pi</category>
      <category>cli</category>
      <category>provider</category>
    </item>
    <item>
      <title>How to Use Upptime to Build Your Own Status Page for Free</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Thu, 18 Jun 2026 05:30:22 +0000</pubDate>
      <link>https://dev.to/newbe36524/how-to-use-upptime-to-build-your-own-status-page-for-free-2ipc</link>
      <guid>https://dev.to/newbe36524/how-to-use-upptime-to-build-your-own-status-page-for-free-2ipc</guid>
      <description>&lt;h1&gt;
  
  
  How to Use Upptime to Build Your Own Status Page for Free
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Move monitoring entirely into a GitHub repo—Actions as probes, the repo as a database, Pages as a CDN, and Issues as an event log. Zero servers, zero monthly fees, yet somehow you end up with a status page that you can view, query, and trace. Call it black magic or the wisdom of the frugal—either way, it runs.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;When operating a small product matrix consisting of over a dozen external services, "is it actually working" became a common refrain. Customers report they can't access it, you SSH in and &lt;code&gt;curl&lt;/code&gt; once to find it's fine; a few minutes later it goes down, but this time you weren't watching. Commercial monitoring (Pingdom, UptimeRobot's premium tier, Datadog) can certainly solve this, but they charge either by site or by request count—for an indie developer, neither cost nor mental burden is quite worth it.&lt;/p&gt;

&lt;p&gt;More importantly, users need to be able to check the status page themselves. Ideally, one domain (say &lt;code&gt;status.hagicode.com&lt;/code&gt;) would display real-time availability for each service, response time curves, historical events, and automatically log incidents when failures occur with automatic notifications. The traditional approach requires four pieces—a server running cron, a database storing historical data, a frontend site, and a CDN. Once you lay out these four, operational costs immediately outweigh the services being monitored—using a sledgehammer to crack a nut, and even the nut feels crowded.&lt;/p&gt;

&lt;p&gt;To address these pain points, we made a decision: move the entire monitoring solution directly to GitHub. This decision brought changes larger than you might imagine—I'll get to that gradually.&lt;/p&gt;

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

&lt;p&gt;The solution shared in this article comes from our experience building &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt;. HagiCode is an AI code assistant project that exposes over a dozen public services including web pages, documentation sites, and download endpoints, driven by the main repository &lt;a href="https://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode-org/site&lt;/a&gt;. These sites must remain stable and available, so status monitoring is not optional for us—it's a necessity. The Upptime solution below is exactly what HagiCode's production environment uses—I didn't make this up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analysis: How Upptime Actually Works
&lt;/h2&gt;

&lt;p&gt;Upptime is essentially a GitHub repository template plus six workflows generated by that template. The key to understanding it is seeing clearly "who calls whom, when, and produces what that lands where." Break it apart and it's not so mysterious.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Flow: One Configuration File Drives Everything
&lt;/h3&gt;

&lt;p&gt;The entire system revolves around a single declarative configuration file &lt;code&gt;.upptimerc.yml&lt;/code&gt;. HagiCode's actual configuration structure looks roughly like this:&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;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HagiCode-org&lt;/span&gt;
&lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upptime&lt;/span&gt;

&lt;span class="na"&gt;sites&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;HagiCode Website&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://hagicode.com&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;HagiCode Docs&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://docs.hagicode.com&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;Server Package Index&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://index.hagicode.com/server/index.json&lt;/span&gt;
  &lt;span class="c1"&gt;# ... 14 sites total&lt;/span&gt;

&lt;span class="na"&gt;status-website&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status.hagicode.com&lt;/span&gt;
  &lt;span class="na"&gt;logoUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://raw.githubusercontent.com/HagiCode-org/upptime/master/assets/upptime-icon.svg&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;HagiCode Status&lt;/span&gt;
  &lt;span class="na"&gt;introTitle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**HagiCode&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Status**"&lt;/span&gt;
  &lt;span class="na"&gt;introMessage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Real-time availability tracking for public HagiCode websites and download endpoints.&lt;/span&gt;
  &lt;span class="na"&gt;navbar&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Status&lt;/span&gt;
      &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GitHub&lt;/span&gt;
      &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/$OWNER/$REPO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two points here are worth calling out. First, &lt;code&gt;sites&lt;/code&gt; can monitor both web pages (returning HTML) and pure JSON endpoints (like &lt;code&gt;index.json&lt;/code&gt;)—Upptime only cares about HTTP status codes and response time, not content validation. Second, &lt;code&gt;cname&lt;/code&gt; points to &lt;code&gt;status.hagicode.com&lt;/code&gt;, which requires you to own that domain and configure DNS to point to GitHub Pages—after all, freebies aside, you still need to provide your own domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Division of Six Workflows
&lt;/h3&gt;

&lt;p&gt;All files under &lt;code&gt;.github/workflows/&lt;/code&gt; have a warning at the top &lt;code&gt;Do not edit this file directly!&lt;/code&gt;—they're auto-updated weekly by the template; you only need to modify &lt;code&gt;.upptimerc.yml&lt;/code&gt;. Each workflow is triggered by cron, calling different subcommands of the same action &lt;code&gt;upptime/uptime-monitor@v1.42.6&lt;/code&gt; with clear division of labor, which is actually quite reassuring:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workflow&lt;/th&gt;
&lt;th&gt;cron&lt;/th&gt;
&lt;th&gt;command&lt;/th&gt;
&lt;th&gt;purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uptime.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*/5 * * * *&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;probe every 5 minutes, write &lt;code&gt;history/*.yml&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;response-time.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;response-time&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;calculate response time statistics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;graphs.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;graphs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;generate day/week/month/year PNG curves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;summary.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;summary&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;update status table in README&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;site.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 1 * * *&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;site&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;build static site daily, deploy to Pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;update-template.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0 * * *&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;sync upstream template weekly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The core snippet of &lt;code&gt;uptime.yml&lt;/code&gt; shows how the "probe" runs:&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&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;*/5&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="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
&lt;span class="na"&gt;jobs&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="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&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@v4&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;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GH_PAT || github.token }}&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;Check endpoint status&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;upptime/uptime-monitor@v1.42.6&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;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;update"&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;GH_PAT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GH_PAT || github.token }}&lt;/span&gt;
          &lt;span class="na"&gt;SECRETS_CONTEXT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ toJson(secrets) }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;site.yml&lt;/code&gt; adds one more step, using &lt;code&gt;peaceiris/actions-gh-pages@v4&lt;/code&gt; to push build artifacts to the &lt;code&gt;gh-pages&lt;/code&gt; branch:&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;peaceiris/actions-gh-pages@v4&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;github_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GH_PAT || github.token }}&lt;/span&gt;
    &lt;span class="na"&gt;publish_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;site/status-page/__sapper__/export/"&lt;/span&gt;
    &lt;span class="na"&gt;user_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Upptime&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Bot"&lt;/span&gt;
    &lt;span class="na"&gt;user_email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;73812536+upptime-bot@users.noreply.github.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Data Persistence: Files as Database
&lt;/h3&gt;

&lt;p&gt;Monitoring results aren't stored in a database; instead they're committed back to the repository as files. This sounds a bit wild, but it's actually solid in practice. Each site produces three types of artifacts.&lt;/p&gt;

&lt;p&gt;Status snapshot &lt;code&gt;history/{slug}.yml&lt;/code&gt;, for example &lt;code&gt;history/hagi-code-website.yml&lt;/code&gt;:&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;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://hagicode.com&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;up&lt;/span&gt;
&lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;
&lt;span class="na"&gt;responseTime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;96&lt;/span&gt;
&lt;span class="na"&gt;lastUpdated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-06-17T00:22:34.485Z&lt;/span&gt;
&lt;span class="na"&gt;startTime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-03-24T10:07:32.531Z&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;shields.io endpoint badge data sources &lt;code&gt;api/{slug}/response-time.json&lt;/code&gt;, &lt;code&gt;uptime.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="nl"&gt;"schemaVersion"&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;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"response time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"739 ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"yellow"&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;And response time curve graphs &lt;code&gt;graphs/{slug}/response-time-{day,week,month,year}.png&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The trade-off of this "files as database" approach is well-considered: write-heavy, read-light, controllable scale (about 288 samples per day per site, storing increments rather than full logs), naturally versioned, zero infrastructure. The cost, of course, is that the repository grows continuously and you occasionally need to look back and care about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Events and Notifications: Issues as Event Log
&lt;/h3&gt;

&lt;p&gt;Incident logging relies on GitHub Issues, paired with two built-in repository templates: &lt;code&gt;.github/ISSUE_TEMPLATE/bug_report.md&lt;/code&gt; (user-reported issues) and &lt;code&gt;maintainance-event.md&lt;/code&gt; (scheduled maintenance). The maintenance template uses frontmatter to express the time window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&amp;lt;!--
start: 2021-08-24T13:00:00.220Z
end: 2021-08-24T14:00:00.220Z
expectedDown: google, hacker-news
--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upptime parses these Issues and renders "maintenance in progress" and "past events" to the status page and README. Notifications rely on Issues' own watch mechanism, plus configurable webhooks, Slack, Telegram (declare &lt;code&gt;notifications&lt;/code&gt; at the top of &lt;code&gt;.upptimerc.yml&lt;/code&gt;; HagiCode's example repo currently doesn't have this enabled—after all, one less thing is one less thing).&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution: Five Steps to Replicate a Status Page
&lt;/h2&gt;

&lt;p&gt;Replicating a HagiCode-style status page, from zero to live, takes five steps total. Five steps sounds like a lot, but each one is short—take your time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create Repository from Template
&lt;/h3&gt;

&lt;p&gt;Don't &lt;code&gt;git clone&lt;/code&gt; and modify—use GitHub's "Use this template" to create a repository directly (e.g., &lt;code&gt;your-org/upptime&lt;/code&gt;). The template already includes all workflows, Issue templates, and the static site skeleton. After cloning locally, the only thing you need to manually modify is &lt;code&gt;.upptimerc.yml&lt;/code&gt;—leave everything else alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Edit &lt;code&gt;.upptimerc.yml&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Change &lt;code&gt;owner&lt;/code&gt;/&lt;code&gt;repo&lt;/code&gt; to yours, list the addresses you want to monitor in &lt;code&gt;sites&lt;/code&gt;, configure &lt;code&gt;status-website&lt;/code&gt; for the site. The minimum viable version looks roughly like this:&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;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-org&lt;/span&gt;
&lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upptime&lt;/span&gt;

&lt;span class="na"&gt;sites&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;Main Site&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://example.com&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;API Health&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.example.com/health&lt;/span&gt;
    &lt;span class="na"&gt;expectedStatusCodes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;

&lt;span class="na"&gt;status-website&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status.example.com&lt;/span&gt;   &lt;span class="c1"&gt;# delete if no domain, use default your-org.github.io/upptime&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;Example Status&lt;/span&gt;
  &lt;span class="na"&gt;introTitle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**Example&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Status**"&lt;/span&gt;
  &lt;span class="na"&gt;introMessage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;服务可用性实时监控&lt;/span&gt;
  &lt;span class="na"&gt;navbar&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Status&lt;/span&gt;
      &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GitHub&lt;/span&gt;
      &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/$OWNER/$REPO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Advanced items: &lt;code&gt;expectedStatusCodes&lt;/code&gt; limits acceptable status codes (default 200-399); &lt;code&gt;headers&lt;/code&gt; customizes request headers (for endpoints requiring auth); &lt;code&gt;maxResponseTime&lt;/code&gt; marks slow responses. Use these as needed—take what you need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Configure Secrets and Permissions
&lt;/h3&gt;

&lt;p&gt;The workflow defaults to &lt;code&gt;${{ secrets.GH_PAT || github.token }}&lt;/code&gt;. &lt;code&gt;github.token&lt;/code&gt; can run the basic flow, but two limitations will bite you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Workflows triggered by the default token won't trigger downstream workflows (to prevent loops), breaking the "probe → create Issue → notify" chain in the middle.&lt;/li&gt;
&lt;li&gt;Insufficient permissions for cross-repo operations (like multi-org).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Recommend creating a new PAT (requires &lt;code&gt;repo&lt;/code&gt; + &lt;code&gt;workflow&lt;/code&gt; permissions) and storing it as repository Secret &lt;code&gt;GH_PAT&lt;/code&gt;. &lt;code&gt;update-template.yml&lt;/code&gt; has a specific check: without &lt;code&gt;GH_PAT&lt;/code&gt;, it skips template auto-update and prints a warning, so this secret isn't just optional—it's key to peace of mind.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Enable GitHub Pages
&lt;/h3&gt;

&lt;p&gt;Repository Settings → Pages → Source select &lt;code&gt;Deploy from a branch&lt;/code&gt;, branch select &lt;code&gt;gh-pages&lt;/code&gt;, directory &lt;code&gt;/root&lt;/code&gt;. &lt;code&gt;site.yml&lt;/code&gt; automatically pushes build artifacts to this branch every day at 1 AM. If you configured &lt;code&gt;cname&lt;/code&gt;, add a CNAME record at your DNS provider pointing to &lt;code&gt;your-org.github.io&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's also good to manually trigger once the first time: Actions page find "Static Site CI" → Run workflow, no need to wait foolishly for scheduled tasks—seeing results one second earlier means peace of mind one second earlier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Verification and Maintenance
&lt;/h3&gt;

&lt;p&gt;After pushing the config, go to Actions and check if "Uptime CI" runs every 5 minutes and if &lt;code&gt;history/&lt;/code&gt; starts showing &lt;code&gt;*.yml&lt;/code&gt; files. The status page address is &lt;code&gt;https://&amp;lt;your-org&amp;gt;.github.io/upptime/&lt;/code&gt; or your custom domain. Later, adding sites or changing domains only requires touching &lt;code&gt;.upptimerc.yml&lt;/code&gt; one file—workflows are fully automated. HagiCode has maintained availability for 14 endpoints using this mechanism for over a year now, basically worry-free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice: I've Walked Through the Potholes for You
&lt;/h2&gt;

&lt;p&gt;The following items are experience accumulated from HagiCode's actual operation—written down so you can take fewer detours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practice 1: Monitoring Granularity Selection
&lt;/h3&gt;

&lt;p&gt;HagiCode puts web pages (&lt;code&gt;https://hagicode.com&lt;/code&gt;) and pure data endpoints (&lt;code&gt;https://index.hagicode.com/server/index.json&lt;/code&gt;) in the same &lt;code&gt;sites&lt;/code&gt; list. For JSON endpoints, Upptime makes requests and parses HTTP status codes but doesn't validate content structure. If you need deep checks like "returns 200 but content is wrong," you'll need to supplement with &lt;code&gt;expectedStatusCodes&lt;/code&gt; plus external probes—Upptime itself only does black-box HTTP checks—it reads the face, not the mind.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practice 2: Clever Use of Response Time Badges
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;api/{slug}/response-time.json&lt;/code&gt; is a &lt;a href="https://shields.io/endpoint" rel="noopener noreferrer"&gt;shields.io endpoint badge&lt;/a&gt; data source. HagiCode's README heavily references these URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FHagiCode-org%2Fupptime%2FHEAD%2Fapi%2Fhagi-code-website%2Fresponse-time.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, you can embed real-time response time badges in any Markdown (project README, blog, third-party pages), with colors driven by the values in &lt;code&gt;message&lt;/code&gt; and the &lt;code&gt;color&lt;/code&gt; field. Note that using &lt;code&gt;HEAD&lt;/code&gt; rather than &lt;code&gt;master&lt;/code&gt;/&lt;code&gt;main&lt;/code&gt; to reference raw files avoids widespread failures after branch renames—details hide stability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practice 3: Repository Size Control
&lt;/h3&gt;

&lt;p&gt;Sampling every 5 minutes, &lt;code&gt;history/&lt;/code&gt; accumulates considerable volume over a year. Upptime uses incremental YAML rather than full logs, which is relatively restrained, but it's still recommended to occasionally check repository size. If a site's monitoring value declines, remove it from &lt;code&gt;sites&lt;/code&gt;—corresponding historical files can also be manually cleaned up; after all, if you can't bear to delete, the repository will eventually show you what bloating means.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practice 4: Real Usage of Maintenance Events
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;maintainance-event.md&lt;/code&gt; isn't decoration. Before a planned release, create an Issue using the template, fill in &lt;code&gt;start/end/expectedDown&lt;/code&gt;, and Upptime will mark corresponding sites during that period as "scheduled maintenance," not counting them toward availability statistics, avoiding dragging down the full-year SLA with a normal release. HagiCode's &lt;code&gt;expectedDown&lt;/code&gt; supports comma-separated site name lists, corresponding one-to-one with &lt;code&gt;sites[].name&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practice 5: Boundaries Between Template Updates and Customization
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Do not edit this file directly!&lt;/code&gt; at the top of all &lt;code&gt;.github/workflows/*.yml&lt;/code&gt; isn't meant to scare you. &lt;code&gt;update-template.yml&lt;/code&gt; overwrites these files with the upstream template weekly. When you need custom behavior, the correct approach is to use officially supported config items in &lt;code&gt;.upptimerc.yml&lt;/code&gt; (like &lt;code&gt;skipTopics&lt;/code&gt;, &lt;code&gt;customStatusWebsite&lt;/code&gt;, &lt;code&gt;runnerSettings&lt;/code&gt;), not to modify workflows. If you really must modify workflows, either turn off &lt;code&gt;update-template.yml&lt;/code&gt; or fork and maintain the template yourself—the latter loses painless upgrades; weigh the trade-offs yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practice 6: Reality Constraints of Free Quotas
&lt;/h3&gt;

&lt;p&gt;GitHub Actions is free and unlimited for public repositories, which Upptime's design exploits. Private repositories have 2000 free minutes per month, while &lt;code&gt;uptime.yml&lt;/code&gt; runs every 5 minutes for about 1 minute each time—just this one item costs about 8640 minutes per month, exceeding the quota. So the Upptime repository must be public—this is the premise of "free"—don't go private for secrecy and then receive a bill, that's awkward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Returning to the initial question: monitoring a pile of external services—is there really a cheap solution? HagiCode's answer is—yes, and so cheap you'll doubt it's real. Upptime breaks monitoring into four GitHub native components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Probes = GitHub Actions cron&lt;/li&gt;
&lt;li&gt;Database = YAML/JSON files in the repository&lt;/li&gt;
&lt;li&gt;CDN = GitHub Pages&lt;/li&gt;
&lt;li&gt;Event log = GitHub Issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You get: real-time availability, response time curves, historical events, availability badges, custom domains, automatic notifications—all with zero servers and zero monthly fees. The cost is keeping the repository public and occasionally caring about repository size. Compared to hand-rolling a monitoring solution, this cost is actually far lighter.&lt;/p&gt;

&lt;p&gt;The reason this solution works is behind GitHub's ecosystem's sincere subsidy for open-source projects. If you're also maintaining a small multi-site product matrix, I strongly recommend spending an afternoon setting this up—it's far less worry than hand-rolling monitoring.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/upptime/upptime" rel="noopener noreferrer"&gt;Upptime official repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://shields.io/endpoint" rel="noopener noreferrer"&gt;shields.io endpoint badge documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule" rel="noopener noreferrer"&gt;GitHub Actions scheduled task documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://status.hagicode.com" rel="noopener noreferrer"&gt;HagiCode status page example&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Around "How to use Upptime to build your own status page for free," a more prudent approach is to gradually get key configurations, dependency boundaries, and landing paths working first, then fill in optimization details.&lt;/p&gt;

&lt;p&gt;Once goals, steps, and acceptance criteria are clear, such solutions can typically enter actual delivery more smoothly.&lt;/p&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-06-18-how-to-use-upptime-for-free-status-page%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-18-how-to-use-upptime-for-free-status-page%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>upptime</category>
      <category>githubactions</category>
      <category>githubpages</category>
      <category>hagicode</category>
    </item>
    <item>
      <title>How to Publish an Electron App to Microsoft Store: From MSIX Packaging to Store Submission</title>
      <dc:creator>Hagicode</dc:creator>
      <pubDate>Thu, 18 Jun 2026 02:58:33 +0000</pubDate>
      <link>https://dev.to/newbe36524/how-to-publish-an-electron-app-to-microsoft-store-from-msix-packaging-to-store-submission-3imn</link>
      <guid>https://dev.to/newbe36524/how-to-publish-an-electron-app-to-microsoft-store-from-msix-packaging-to-store-submission-3imn</guid>
      <description>&lt;h1&gt;
  
  
  How to Publish an Electron App to Microsoft Store: From MSIX Packaging to Store Submission
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;At its core, Electron is just an ordinary Win32 desktop application, but the Microsoft Store only recognizes MSIX. This article, drawing from the actual build configuration we used for HagiCode Desktop, breaks down the entire chain from "registering a developer account → creating MSIX packages → submitting to the store" from start to finish. We'll also discuss the pitfalls we encountered along the way—after all, once you've fallen into the traps, they become stories.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;You have an Electron application that needs to be distributed to end users on Windows. Beyond the NSIS installer packages and portable versions we've always used, we also wanted it available in the Microsoft Store. The reasons are actually quite practical:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trusted Distribution Channel&lt;/strong&gt;: Apps in the store are signed and reviewed, so users won't be blocked by SmartScreen during installation, and they won't have to face that cold "Unknown publisher" warning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Updates and Commercialization&lt;/strong&gt;: The store handles updates for you; subscriptions and permanent licenses can be directly integrated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage of Built-in Windows 10/11 Entry Points&lt;/strong&gt;: winget, store search, Start menu recommendations—these entry points are genuinely useful for user acquisition.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;However, Electron is ultimately not UWP. To publish to the Microsoft Store, the core task is essentially one thing—repackage Electron's output into an &lt;strong&gt;MSIX package&lt;/strong&gt; that the Microsoft Store recognizes, and then complete the registration and submission process dutifully. It sounds simple, but once you actually get started, there are quite a few pitfalls. We spent significant effort mapping out the entire process to fill in these gaps. Below, we'll break down each step in detail.&lt;/p&gt;

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

&lt;p&gt;The solution described in this article comes from our practice in the &lt;a href="https://hagicode.com" rel="noopener noreferrer"&gt;HagiCode&lt;/a&gt; project. HagiCode Desktop is an Electron-based desktop application that needs to be distributed to users through three channels: the official website, GitHub Release, and the Microsoft Store. This article explains how we connected the store channel. There's more information about HagiCode at the end—if you're interested, feel free to scroll down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analysis: Four Critical Questions to Clarify Before Publishing
&lt;/h2&gt;

&lt;p&gt;Publishing to the Microsoft Store involves four key technical judgments. Once you've thought these through clearly, you won't need to repeatedly backtrack later—after all, no one wants to redo their work.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Microsoft Store Only Accepts MSIX / AppX, Not Traditional NSIS/EXE
&lt;/h3&gt;

&lt;p&gt;The Microsoft Store's support for desktop applications (Desktop Bridge) is built on the MSIX format. Traditional NSIS installer packages cannot be submitted directly; they must first be repackaged into MSIX using &lt;code&gt;MakeAppx&lt;/code&gt;. Fortunately, Electron Forge provides a &lt;code&gt;@electron-forge/maker-msix&lt;/code&gt; maker that can produce MSIX directly during the packaging phase, saving you the hassle of reverse-engineering packaging from an installed directory.&lt;/p&gt;

&lt;p&gt;Our project includes this maker:&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="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@electron-forge/maker-msix&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;platforms&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;win32&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;appManifest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msixManifestPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;packageAssets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msixAssetsPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;logLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warn&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="nx"&gt;windowsKitPath&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;windowsKitPath&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="nx"&gt;windowsKitVersion&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;windowsKitVersion&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="nx"&gt;msixSigningConfig&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 key inputs are essentially two: &lt;code&gt;appManifest&lt;/code&gt; (AppxManifest.xml, which defines package identity and capabilities) and &lt;code&gt;packageAssets&lt;/code&gt; (store icon assets). If either of these is wrong, everything else, no matter how well done, is wasted effort.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Package Identity Must Be Reserved in Partner Center in Advance
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Identity&lt;/code&gt; field (&lt;code&gt;Name&lt;/code&gt;, &lt;code&gt;Publisher&lt;/code&gt;) in the MSIX package cannot be filled in arbitrarily—it must match exactly the application identity reserved in Partner Center. Even one character off will result in rejection. Our reserved identity is stored in &lt;code&gt;forge.store-config.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;"packageIdentity"&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;"displayName"&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"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"publisherDisplayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"newbe36524"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"publisher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CN=8B6C8A94-AAE5-4C8B-9202-A29EA42B042F"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"identityName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"newbe36524.Hagicode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"backgroundColor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"transparent"&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="s2"&gt;"en-US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zh-CN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zh-TW"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ja-JP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ko-KR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"de-DE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fr-FR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"es-ES"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pt-BR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ru-RU"&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;That &lt;code&gt;publisher&lt;/code&gt; string comes from the certificate subject issued by Microsoft after developer account registration and must match character-for-character. The &lt;code&gt;identityName&lt;/code&gt; is the package name prefix you reserved. This string must be copied exactly from Partner Center—never type it manually. We'll discuss this again in the "Common Pitfalls" section later.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Desktop Applications Must Declare &lt;code&gt;runFullTrust&lt;/code&gt; Capability
&lt;/h3&gt;

&lt;p&gt;Electron applications need full file system access, need to spawn child processes, and need to run the Node runtime—these can only be achieved in "full trust" mode. Therefore, the MSIX manifest must honestly declare the &lt;code&gt;runFullTrust&lt;/code&gt; capability, otherwise the application will be blocked by the sandbox upon startup, manifesting as various baffling crashes. Our configuration looks like this:&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;"msix"&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;"minVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.0.17763.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"maxVersionTested"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.0.19045.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"capabilities"&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;"runFullTrust"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"internetClient"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"internetClientServer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"privateNetworkClientsServer"&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;&lt;code&gt;runFullTrust&lt;/code&gt; is the standard for desktop application publishing. Setting &lt;code&gt;minVersion&lt;/code&gt; to 17763 (Windows 10 1809) is because from this version onwards, MSIX has stable support for desktop Win32 applications; set it lower and users can't install it; set it higher and you won't cover those older machines.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Store Submission Requires Windows Environment + Microsoft Store CLI
&lt;/h3&gt;

&lt;p&gt;Packaging can be done on cross-platform CI, but store submission (&lt;code&gt;msstore publish&lt;/code&gt;) cannot—it must run the Microsoft Store CLI in a Windows environment with Azure AD application credentials configured. This is why the &lt;code&gt;publish_store&lt;/code&gt; job in the automation pipeline must run on a &lt;code&gt;windows-latest&lt;/code&gt; runner. This is a hard constraint that can't be bypassed, unlike packaging which can be stuffed into a Linux container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution: Eight-Step Process for Complete Store Publishing
&lt;/h2&gt;

&lt;p&gt;Connecting the analysis above, the complete steps to publish an Electron app to the Microsoft Store are roughly as follows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Register a Developer Account
&lt;/h3&gt;

&lt;p&gt;First, go to &lt;a href="https://partner.microsoft.com/" rel="noopener noreferrer"&gt;Partner Center&lt;/a&gt; to register a developer account (individual or company) and pay the one-time fee. After the account is activated, you'll receive a &lt;strong&gt;Publisher certificate subject string&lt;/strong&gt;, which looks roughly like this: &lt;code&gt;CN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX&lt;/code&gt;. This is the only source for the &lt;code&gt;publisher&lt;/code&gt; field later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Reserve Application Identity in the Store
&lt;/h3&gt;

&lt;p&gt;Create a new application in Partner Center and fill in the name you want to reserve. The system will assign you an &lt;code&gt;identityName&lt;/code&gt;, which combined with your own Publisher forms the complete package identity. Copy this identity exactly into your local 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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;forge.store-config.json&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;"packageIdentity"&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;"displayName"&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"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"publisherDisplayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"newbe36524"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"publisher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CN=8B6C8A94-AAE5-4C8B-9202-A29EA42B042F"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"identityName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"newbe36524.Hagicode"&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;
  
  
  Step 3: Prepare Store Icon Assets
&lt;/h3&gt;

&lt;p&gt;The Microsoft Store requires a set of PNGs in fixed dimensions: &lt;code&gt;StoreLogo.png&lt;/code&gt;, &lt;code&gt;Square44x44Logo.png&lt;/code&gt;, &lt;code&gt;Square150x150Logo.png&lt;/code&gt;, &lt;code&gt;Wide310x150Logo.png&lt;/code&gt;, etc. Our &lt;code&gt;prepare-msix.js&lt;/code&gt; script validates whether all these assets are present before packaging:&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;// Validate required store icon assets, missing even one won't work&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requiredAssets&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;StoreLogo.png&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;Square44x44Logo.png&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;Square150x150Logo.png&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;Wide310x150Logo.png&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;assetName&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;requiredAssets&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;assetPath&lt;/span&gt; &lt;span class="o"&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;paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;generatedAssetsPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;assetName&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;assetPath&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;`Missing required MSIX asset after preparation: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;assetPath&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why do this? Because if one size is missing, MakeAppx won't tell you exactly what's wrong during packaging, and you'll only get rejected during store review—by which time you've already waited several days. Validating in advance is an effective defense.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Generate AppxManifest.xml
&lt;/h3&gt;

&lt;p&gt;The manifest needs to include package identity, capabilities, visual assets, and the entry executable. We use an override configuration (&lt;code&gt;forge.store-config.json&lt;/code&gt;) to drive &lt;code&gt;prepare-msix.js&lt;/code&gt; to generate the manifest, ensuring the identity matches the store. The key sections of the manifest look roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Package identity: must match Partner Center --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Identity&lt;/span&gt; &lt;span class="na"&gt;Name=&lt;/span&gt;&lt;span class="s"&gt;"newbe36524.Hagicode"&lt;/span&gt;
          &lt;span class="na"&gt;Publisher=&lt;/span&gt;&lt;span class="s"&gt;"CN=8B6C8A94-AAE5-4C8B-9202-A29EA42B042F"&lt;/span&gt;
          &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.2.3.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;Applications&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Application&lt;/span&gt; &lt;span class="na"&gt;Id=&lt;/span&gt;&lt;span class="s"&gt;"Hagicode"&lt;/span&gt; &lt;span class="na"&gt;Executable=&lt;/span&gt;&lt;span class="s"&gt;"Hagicode.exe"&lt;/span&gt; &lt;span class="na"&gt;EntryPoint=&lt;/span&gt;&lt;span class="s"&gt;"Windows.FullTrustApplication"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;uap:VisualElements&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/Application&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Applications&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Capability declaration: runFullTrust is key for desktop apps --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Capabilities&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;rescap:Capability&lt;/span&gt; &lt;span class="na"&gt;Name=&lt;/span&gt;&lt;span class="s"&gt;"runFullTrust"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Capability&lt;/span&gt; &lt;span class="na"&gt;Name=&lt;/span&gt;&lt;span class="s"&gt;"internetClientServer"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Capabilities&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that line &lt;code&gt;EntryPoint="Windows.FullTrustApplication"&lt;/code&gt;—this is a critical marker for desktop applications. Combined with the &lt;code&gt;runFullTrust&lt;/code&gt; capability, it allows the app to run with full permissions. Without it, the app is confined to the sandbox, quite frustratingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Package with maker-msix
&lt;/h3&gt;

&lt;p&gt;The build command is in &lt;code&gt;package.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;"scripts"&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;"build:win:store"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run generate:store-bindings &amp;amp;&amp;amp; node scripts/build-store-package.js"&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;It ultimately calls Electron Forge, passing &lt;code&gt;forge.store-config.json&lt;/code&gt; as an override configuration. The maker-msix will call the Windows SDK's &lt;code&gt;MakeAppx&lt;/code&gt; to output an &lt;code&gt;.msix&lt;/code&gt; file. There's a hard constraint here: &lt;strong&gt;packaging must be done on Windows&lt;/strong&gt; (or in a container with Windows SDK), since it depends on &lt;code&gt;MakeAppx&lt;/code&gt;—this can't be bypassed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Sign (Can Skip for Store Submission)
&lt;/h3&gt;

&lt;p&gt;This step is easily overlooked—packages submitted to the store are re-signed by Microsoft with their own certificates, so for development self-testing outside "formal submission," you don't need to sign. However, if you want to install and test locally, you need to sign with a trusted certificate, otherwise Windows will refuse installation. Our &lt;code&gt;resolveMsixSigningConfig&lt;/code&gt; returns an empty object when no signing materials are configured, letting the process continue:&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;// Don't sign if no signing materials configured, let the store re-sign uniformly&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveMsixSigningConfig&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;process&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;MSIX_CERT_FILE&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;signMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signtool&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;certFilePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&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;MSIX_CERT_FILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;certPassword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&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;MSIX_CERT_PASSWORD&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;Separating the "self-testing signing" and "submitting unsigned" paths is a key practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Configure Microsoft Store CLI Credentials
&lt;/h3&gt;

&lt;p&gt;Create an Azure AD application in the Azure portal, grant it permissions to access Partner Center, and obtain the following set of credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AZURE_AD_APPLICATION_CLIENT_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AZURE_AD_APPLICATION_SECRET&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AZURE_AD_TENANT_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SELLER_ID&lt;/code&gt; (Seller ID from Partner Center)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MICROSOFT_STORE_PRODUCT_ID&lt;/code&gt; (Product ID of the reserved application)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This step is slightly convoluted, but the documentation for both Azure Portal and Partner Center is quite detailed—just follow it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8: Submit to Store
&lt;/h3&gt;

&lt;p&gt;In a Windows environment, submit using the Microsoft Store CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Configure credentials&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;msstore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;reconfigure&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--tenantId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;AZURE_AD_TENANT_ID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--clientId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;AZURE_AD_APPLICATION_CLIENT_ID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--clientSecret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;AZURE_AD_APPLICATION_SECRET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--sellerId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;SELLER_ID&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Submit MSIX package to the reserved product&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;msstore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;publish&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$packagePath&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;MICROSOFT_STORE_PRODUCT_ID&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After submission, you need to return to Partner Center to fill in the store listing details (description, screenshots, pricing, rating), then click submit for review. Review typically takes 1–3 business days; first submissions tend to take longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practice: Consolidating Configuration and Pitfall Experience
&lt;/h2&gt;

&lt;p&gt;After going through the process once, these practices can help you avoid some detours—after all, after you've taken enough detours, they don't feel like detours anymore, but some things can still be skipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Store Configuration Files Separately
&lt;/h3&gt;

&lt;p&gt;Separating "general build configuration" from "store-specific configuration" is key. Our approach is: &lt;code&gt;forge.config.js&lt;/code&gt; handles daily builds (NSIS, portable, macOS dmg), while &lt;code&gt;forge.store-config.json&lt;/code&gt; only extends and overrides during store builds:&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;"extends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"forge.config.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"buildVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packageIdentity"&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;store&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;reserved&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;identity&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"msix"&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;"minVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.0.17763.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"maxVersionTested"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.0.19045.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"capabilities"&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;"runFullTrust"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"internetClient"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"internetClientServer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"privateNetworkClientsServer"&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 way, store builds and release builds don't pollute each other. HagiCode Desktop maintains three distribution channels simultaneously; configuration separation is the prerequisite for our stable iteration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version Numbers Must Be Four Segments
&lt;/h3&gt;

&lt;p&gt;MSIX version numbers must be four segments (&lt;code&gt;Major.Minor.Build.Revision&lt;/code&gt;, e.g., &lt;code&gt;1.2.3.0&lt;/code&gt;), but Electron's &lt;code&gt;package.json&lt;/code&gt; typically only has three segments. That &lt;code&gt;buildVersion&lt;/code&gt; field is used to fill in the last segment—when submitting to the store, version numbers must increment, and the fourth segment is very convenient for distinguishing multiple submissions under the same semantic version. Those who've encountered this will understand; those who haven't will eventually.&lt;/p&gt;

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

&lt;p&gt;The store supports multi-language listings, which in the manifest corresponds to each &lt;code&gt;&amp;lt;Resource Language="..." /&amp;gt;&lt;/code&gt; tag. We declared ten languages, and the store requires filling in a description for each (you can use machine translation to pass review first, then localize gradually). The corresponding rendering logic in &lt;code&gt;prepare-msix.js&lt;/code&gt; 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="c1"&gt;// Render language list as Resource tags in MSIX manifest&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderResourceTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;languages&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;languages&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;language&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`    &amp;lt;Resource Language="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;escapeXml&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="s2"&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="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;
  
  
  Common Pitfalls (Focus Here)
&lt;/h3&gt;

&lt;p&gt;HagiCode Desktop has encountered almost every one of these pitfalls:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Publisher Mismatch&lt;/strong&gt;: When copying the publisher string from Partner Center, it's easy to accidentally lose a space or mess up the case, leading to immediate rejection.建议直接写成配置文件，别手敲。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing &lt;code&gt;runFullTrust&lt;/code&gt;&lt;/strong&gt;: After the app starts, it can't access the file system or spawn child processes, manifesting as various bizarre crashes that are frustrating to debug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incomplete Icon Sizes&lt;/strong&gt;: MakeAppx doesn't validate, but store review will reject it. &lt;code&gt;prepare-msix.js&lt;/code&gt; validating in advance is an effective defense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-Incrementing Version Numbers&lt;/strong&gt;: The store refuses to accept the same or lower version numbers; CI pipelines must ensure bumping on each build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running maker-msix in Non-Windows Environments&lt;/strong&gt;: It won't find &lt;code&gt;MakeAppx&lt;/code&gt;; you must use a &lt;code&gt;windows-latest&lt;/code&gt; runner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signing Confusion&lt;/strong&gt;: Use self-signed certificates for self-testing, submit unsigned for store submission and let Microsoft re-sign—separate these two paths, don't stuff self-signed certificates into submission packages.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Automation Recommendations
&lt;/h3&gt;

&lt;p&gt;After manually going through the entire process once and understanding each step, it's strongly recommended to connect GitHub Actions for automation. We eventually chained version parsing, MSIX building, GitHub Release publishing, and store publishing into a pipeline, checking for new versions every 4 hours. These details are fully broken down in our other article "Automating Windows App Publication to Microsoft Store."&lt;/p&gt;

&lt;p&gt;If you just want to get your app on the store first and integrate commercialization (subscriptions / permanent licenses) later, you can also check out our "How to Integrate Microsoft Store Subscriptions and Permanent Licenses in Electron Desktop Apps"—that article covers connecting commercialization capabilities after store publication.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/microsoft/AppPublisher" rel="noopener noreferrer"&gt;Microsoft Store CLI Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.electronforge.io/config/makers/maker-msix" rel="noopener noreferrer"&gt;electron-forge maker-msix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/windows/msix/" rel="noopener noreferrer"&gt;MSIX Documentation&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://github.com/HagiCode-org/site" rel="noopener noreferrer"&gt;HagiCode-org/site GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Regarding "How to Publish an Electron App to Microsoft Store: From MSIX Packaging to Store Submission," a more prudent approach is to first run through key configurations, dependency boundaries, and implementation paths step by step, then fill in optimization details.&lt;/p&gt;

&lt;p&gt;Once objectives, steps, and acceptance criteria are clear, such solutions can typically enter actual delivery more smoothly.&lt;/p&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-06-18-electron-app-publish-to-microsoft-store%2F" rel="noopener noreferrer"&gt;https://docs.hagicode.com/go?platform=devto&amp;amp;target=%2Fblog%2F2026-06-18-electron-app-publish-to-microsoft-store%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>electron</category>
      <category>microsoftstore</category>
      <category>msix</category>
      <category>windows</category>
    </item>
  </channel>
</rss>
