<?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: Guilherme Brasil</title>
    <description>The latest articles on DEV Community by Guilherme Brasil (@gbrasil720).</description>
    <link>https://dev.to/gbrasil720</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%2F658861%2F4a3e5cc9-87e8-42bb-bd1f-84707ece7322.png</url>
      <title>DEV Community: Guilherme Brasil</title>
      <link>https://dev.to/gbrasil720</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gbrasil720"/>
    <language>en</language>
    <item>
      <title>How I built a multi-agent simulator as a native SwiftUI app</title>
      <dc:creator>Guilherme Brasil</dc:creator>
      <pubDate>Mon, 19 Oct 2026 17:56:09 +0000</pubDate>
      <link>https://dev.to/gbrasil720/how-i-built-a-multi-agent-simulator-as-a-native-swiftui-app-3h61</link>
      <guid>https://dev.to/gbrasil720/how-i-built-a-multi-agent-simulator-as-a-native-swiftui-app-3h61</guid>
      <description>&lt;p&gt;I built &lt;strong&gt;Sorcerer&lt;/strong&gt;, a free, open-source macOS app (SwiftUI, macOS 15+, MIT) that simulates a community of AI characters reacting to a "what if…?" question. You write a hypothesis ("what if Lumen raised its plan from $9 to $14 a month?"), summon a cast, and watch them post, reply, react, change their minds and take actions, round by round.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/gbrasil720/sorcerer" rel="noopener noreferrer"&gt;https://github.com/gbrasil720/sorcerer&lt;/a&gt; · Site: &lt;a href="https://sorcererapp.dev" rel="noopener noreferrer"&gt;https://sorcererapp.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post covers how it works under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. No API keys: drive the CLIs people already have
&lt;/h2&gt;

&lt;p&gt;I didn't want to ship a model or ask anyone for a key. Sorcerer runs the official tools the user is already signed in to: the &lt;code&gt;claude&lt;/code&gt; CLI (Claude Code), the &lt;code&gt;codex&lt;/code&gt; CLI (ChatGPT), or a local Ollama.&lt;/p&gt;

&lt;p&gt;The first problem: an app launched from Finder doesn't get your Terminal's &lt;code&gt;PATH&lt;/code&gt;, so &lt;code&gt;claude&lt;/code&gt; is "command not found". Sorcerer asks an interactive zsh once and remembers the answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// The PATH your Terminal has. A login shell alone misses what ~/.zshrc adds (~/.local/bin, where&lt;/span&gt;
&lt;span class="c1"&gt;/// the claude installer puts it; nvm, bun…), so ask an interactive shell once and remember it.&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;userPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executableURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;fileURLWithPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"/bin/zsh"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"-ilc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="s"&gt;"printf '&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;__SORCERER_PATH__%s' "&lt;/span&gt;&lt;span class="n"&gt;$PATH&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also has a 5-second deadline, because an interactive shell can hang on a prompt or plugin.&lt;/p&gt;

&lt;p&gt;Then each provider is one &lt;code&gt;switch&lt;/code&gt; case. Claude runs with no tools, no session file and no user settings. It uses &lt;code&gt;stream-json&lt;/code&gt; because that output also carries the subscription's 5-hour and weekly usage windows, so the app can pause &lt;em&gt;before&lt;/em&gt; you hit a limit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="s"&gt;"claude -p --tools '' --no-session-persistence --setting-sources '' --output-format stream-json --verbose&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;pick&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onText&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;" --include-partial-messages"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codex runs with &lt;code&gt;codex exec --skip-git-repo-check --ephemeral -s read-only … -o &amp;lt;tmpfile&amp;gt; -&lt;/code&gt;. Ollama is a plain &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;http://localhost:11434/api/generate&lt;/code&gt; with &lt;code&gt;"stream": true&lt;/code&gt;, which reads one JSON object per line.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Characters
&lt;/h2&gt;

&lt;p&gt;A character is a plain &lt;code&gt;Codable&lt;/code&gt; struct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;Persona&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Codable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Identifiable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Hashable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;bio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;traits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;interests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;goals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;mood&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;memories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="c1"&gt;/// handle → relation ("coworker", "rivals"). Optional so older worlds still load.&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;ties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;]?&lt;/span&gt;
    &lt;span class="c1"&gt;/// Knows the brief's insider facts (employees, partners, friends of the team).&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;insider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mood&lt;/code&gt; is the character's attitude toward the hypothesis, from -1 to 1. &lt;code&gt;ties&lt;/code&gt; are how influence spreads. &lt;code&gt;memories&lt;/code&gt; are one-liners the model writes each round.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A round is one prompt, and the rules live in pure functions
&lt;/h2&gt;

&lt;p&gt;The simulation lives in &lt;code&gt;Engine.swift&lt;/code&gt;, whose header says "Prompts and parsing. Pure functions, so the simulation rules are testable without a model."&lt;/p&gt;

&lt;p&gt;Each round, &lt;code&gt;roundPrompt&lt;/code&gt; gives the model the whole world:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the cast, with role, mood, ties, last 4 memories and last action&lt;/li&gt;
&lt;li&gt;past events and anything injected this round&lt;/li&gt;
&lt;li&gt;the last 24 posts, each with a stable id like &lt;code&gt;A3&lt;/code&gt; (third post of branch A)&lt;/li&gt;
&lt;li&gt;what characters may know (more on that below)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model plays every character and must reply with only JSON:&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;"posts"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"handle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"replyTo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"post id it answers, e.g. A3, or null"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"text"&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="nl"&gt;"mood"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="nl"&gt;"reactions"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"A3"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"agree"&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="nl"&gt;"disagree"&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;"memories"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"handle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"one short line this character will remember"&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 prompt nudges toward realism: not everyone posts every round, opinions shift gradually, replies come most often from people with ties, and quiet characters still react with agree/disagree counts.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;apply&lt;/code&gt; validates the output instead of trusting it. Unknown authors are dropped and moods are clamped. Actions must be on the experiment's allowed list. Only someone who left can "come back", and there's at most one invite per round. Because this is pure code, &lt;code&gt;swift test&lt;/code&gt; covers it without calling a model.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Who knows what
&lt;/h2&gt;

&lt;p&gt;You can point Sorcerer at a GitHub repo (through your own &lt;code&gt;gh&lt;/code&gt; login), a folder, a site or files. It writes a &lt;strong&gt;brief&lt;/strong&gt; where every fact is tagged &lt;code&gt;public&lt;/code&gt;, &lt;code&gt;insider&lt;/code&gt; or &lt;code&gt;hidden&lt;/code&gt;. &lt;code&gt;knowledge()&lt;/code&gt; builds each prompt's knowledge section: insider facts reach only characters marked &lt;code&gt;insider&lt;/code&gt;, hidden ones reach nobody, and it ends with "Characters never quote facts they couldn't know." When reading sources, it only reads docs. &lt;code&gt;.env&lt;/code&gt; files and keys are never opened, and characters see the brief, never your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Streaming half a JSON
&lt;/h2&gt;

&lt;p&gt;To show "&lt;a class="mentioned-user" href="https://dev.to/wren"&gt;@wren&lt;/a&gt; is typing…" with words appearing live, Sorcerer parses the round's JSON &lt;em&gt;while it streams&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// Reads a round's JSON while it streams in. Complete post objects are decoded; the open one&lt;/span&gt;
&lt;span class="c1"&gt;/// yields its author and the text so far. Never throws: half a JSON is the normal case here.&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;partialRound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;text&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Partial&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a small hand-rolled scanner. It finds &lt;code&gt;"posts"&lt;/code&gt;, tracks brace depth and string escapes, decodes every closed &lt;code&gt;{…}&lt;/code&gt; as a finished post, and pulls &lt;code&gt;author&lt;/code&gt; and partial &lt;code&gt;text&lt;/code&gt; out of the open one. Claude (with &lt;code&gt;--include-partial-messages&lt;/code&gt;) and Ollama stream. Codex doesn't yet, and that's &lt;a href="https://github.com/gbrasil720/sorcerer/issues/21" rel="noopener noreferrer"&gt;issue #21&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Forking the world
&lt;/h2&gt;

&lt;p&gt;Forking copies the branch with the same people and the same memories, then starts a new path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;fork&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;experiment&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;letters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ABCDEFGHIJKLMNOPQRSTUVWXYZ"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;init&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;copy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;letters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;letters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"from &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt; at r&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;mutate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;switchBranch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To fork from a &lt;em&gt;past&lt;/em&gt; round, &lt;code&gt;rewind(_:to:start:)&lt;/code&gt; returns the branch as it stood after round &lt;code&gt;n&lt;/code&gt;. It filters posts, events, actions and polls, drops people who joined later, removes memories tagged with later rounds, and rebuilds each mood from the posts. There's an honest comment in there: agree/disagree counts are totals, so reactions from later rounds stay on old posts. That's a known trade-off.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Counted, not guessed
&lt;/h2&gt;

&lt;p&gt;Reports separate what the model &lt;em&gt;judges&lt;/em&gt; from what the world &lt;em&gt;counted&lt;/em&gt;. If six people answer a poll, the report says "would you pay $14 · no · 3 / 6" from the actual answers. An action tally only counts each person's latest action, so "upgrade" then "cancel" counts once, as cancel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;App: pure Swift, SwiftUI, Swift Package, &lt;strong&gt;no third-party dependencies&lt;/strong&gt;. Run it with &lt;code&gt;cd apps/macos &amp;amp;&amp;amp; swift run&lt;/code&gt; and test with &lt;code&gt;swift test&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Landing page: Bun + Turbo monorepo (React, TanStack Router, Tailwind, Hono, tRPC), deployed on Cloudflare Pages.&lt;/li&gt;
&lt;li&gt;Releases: push a &lt;code&gt;vX.Y.Z&lt;/code&gt; tag and CI tests, builds a universal DMG and publishes it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Want to help?
&lt;/h2&gt;

&lt;p&gt;I opened well-scoped issues with context, file pointers and acceptance criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/gbrasil720/sorcerer/issues/15" rel="noopener noreferrer"&gt;#15 Non-default Ollama host&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gbrasil720/sorcerer/issues/16" rel="noopener noreferrer"&gt;#16 Accessibility labels for VoiceOver&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gbrasil720/sorcerer/issues/17" rel="noopener noreferrer"&gt;#17 Add a new experiment template&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gbrasil720/sorcerer/issues/18" rel="noopener noreferrer"&gt;#18 Unit tests for Ollama stream parsing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gbrasil720/sorcerer/issues/19" rel="noopener noreferrer"&gt;#19 Biome and landing build in CI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gbrasil720/sorcerer/issues/20" rel="noopener noreferrer"&gt;#20 Remove leftover scaffold module&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gbrasil720/sorcerer/issues/21" rel="noopener noreferrer"&gt;#21 Stream codex replies (help wanted)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tip: test with Ollama so you don't burn your Claude or ChatGPT limits.&lt;/p&gt;

&lt;p&gt;Simulations surface possible reactions; they don't replace real-world evidence. I'd love to hear where it feels convincing and where it falls apart. Repo: &lt;a href="https://github.com/gbrasil720/sorcerer" rel="noopener noreferrer"&gt;https://github.com/gbrasil720/sorcerer&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>swift</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
