<?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: Krrish Agrawal</title>
    <description>The latest articles on DEV Community by Krrish Agrawal (@krrishagrawal).</description>
    <link>https://dev.to/krrishagrawal</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%2F4069482%2F618c9b6f-93f1-4b72-8696-1854881017d8.jpg</url>
      <title>DEV Community: Krrish Agrawal</title>
      <link>https://dev.to/krrishagrawal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/krrishagrawal"/>
    <language>en</language>
    <item>
      <title>I Built an AI Investment Council with Switch: Making Bull and Bear Researchers Work Together</title>
      <dc:creator>Krrish Agrawal</dc:creator>
      <pubDate>Fri, 06 Nov 2026 17:38:49 +0000</pubDate>
      <link>https://dev.to/krrishagrawal/i-built-an-ai-investment-council-with-switch-making-bull-and-bear-researchers-work-together-2pk3</link>
      <guid>https://dev.to/krrishagrawal/i-built-an-ai-investment-council-with-switch-making-bull-and-bear-researchers-work-together-2pk3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Investor Council puts two Claude Code agents (a Bull and a Bear researcher) and a human portfolio manager in one Switch/Mattermost room. Every claim is typed and tied to hash-checked SEC EDGAR data, a deterministic Python layer renders the final dossier, and the system refuses to finalize until the human has ruled on every flagged claim.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code, docs and demo:&lt;/strong&gt; &lt;a href="https://github.com/Krrish-agrawal/Investor-Council" rel="noopener noreferrer"&gt;https://github.com/Krrish-agrawal/Investor-Council&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Two Claude Code agents argue about a stock in a shared Mattermost room. A Python ledger keeps them honest. A human has to settle every disputed claim. Here is how it works, what broke, and what Switch did and did not do for me.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/Krrish-agrawal/Investor-Council" rel="noopener noreferrer"&gt;Investor Council&lt;/a&gt; · &lt;strong&gt;Hackathon:&lt;/strong&gt; &lt;a href="https://www.wemakedevs.org/hackathons/switch" rel="noopener noreferrer"&gt;WeMakeDevs × Switch "Flip the Switch"&lt;/a&gt;, Track 02 (Personal or Creative Workflow) · &lt;strong&gt;Solo build&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Research and decision support only. Not financial advice.&lt;/strong&gt; Investor Council never recommends buying, selling or holding anything, never touches a brokerage account and never places a trade. Every number in this post comes from a &lt;strong&gt;synthetic&lt;/strong&gt; demo company and a &lt;strong&gt;synthetic&lt;/strong&gt; sample portfolio. I did not run it against live SEC data (more on that below).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who did what.&lt;/strong&gt; I designed Investor Council and built it with Claude Code as my engineering partner. The live run described below (session &lt;strong&gt;S-0004&lt;/strong&gt;, Nov 5) was an end-to-end verification in which Claude Code played the human "test operator": it pasted the stage messages into Mattermost, ran &lt;code&gt;advance&lt;/code&gt;, &lt;code&gt;rule&lt;/code&gt; and &lt;code&gt;finalize&lt;/code&gt;, and wrote the rulings and the decision note. I had asked it to carry out the whole workflow. The dossier labels those rulings as test-run rulings. I have not yet recorded a run in which a person makes the calls, so where this post describes human governance, it describes what the system enforces and what the test run exercised, not anyone's real investment decisions.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The problem: a chatbot that agrees with you is not research
&lt;/h2&gt;

&lt;p&gt;If you manage your own money and ask a single LLM "is this stock a good compounder?", three things tend to go wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It ignores your portfolio.&lt;/strong&gt; The useful question is rarely "is this a good company?" It is "what would this position do to &lt;em&gt;my&lt;/em&gt; concentration and overlap?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It blends facts, guesses and persuasion into one fluent answer.&lt;/strong&gt; You cannot tell which sentence is a filing figure and which is a vibe, and sources are rarely shown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You are passive.&lt;/strong&gt; You get a verdict. You don't get to interrogate a disagreement and decide which side's claims you accept.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wanted the opposite: two agents with different standing priors, a rule that every number must trace to a source, a visible argument between them, and a human who rules on what is allowed to stand. The output is a dossier that records &lt;em&gt;the human's&lt;/em&gt; conclusion and every gap, not an agent's recommendation.&lt;/p&gt;

&lt;p&gt;That is Investor Council. It is also, I think, a decent first project for learning Switch, because the interesting part is not the agents. It is the room they share.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Switch is (if you have never used it)
&lt;/h2&gt;

&lt;p&gt;Switch is a way to put people and AI agents in the same chat room and have them work together. The pieces that matter for this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Switch Console&lt;/strong&gt; is a desktop app. On my Mac it ran a local Switch server (in Console 0.40.1 that server runs without Docker) and a bundled &lt;strong&gt;Mattermost&lt;/strong&gt; instance (which does need Docker), and it starts a Claude Code session on my machine when one of my agents is addressed. (In Console 0.40 the agents it runs can also be moved onto a "managed machine", which is the same computer; see lesson 5.) (&lt;a href="https://docs.switchagents.ai/switch-rooms/getting-started/install-switch-console" rel="noopener noreferrer"&gt;Install Switch Console&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An agent&lt;/strong&gt; is a named thing with a working directory, a run location, a provider (I used Claude Code) and standing &lt;strong&gt;Agent instructions&lt;/strong&gt;. Names and directories are fixed at creation. (&lt;a href="https://docs.switchagents.ai/switch-rooms/getting-started/add-a-second-agent" rel="noopener noreferrer"&gt;Add an agent&lt;/a&gt;, &lt;a href="https://docs.switchagents.ai/switch-rooms/getting-started/agent-settings" rel="noopener noreferrer"&gt;Agent settings&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A room&lt;/strong&gt; is a channel in Mattermost with &lt;strong&gt;Room instructions&lt;/strong&gt; that every agent reads when it joins. (&lt;a href="https://docs.switchagents.ai/switch-rooms/getting-started/create-a-room" rel="noopener noreferrer"&gt;Create a room&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You &lt;code&gt;@mention&lt;/code&gt; an agent&lt;/strong&gt; to give it work. An agent acts when someone with permission addresses it, and ignores messages that don't. (&lt;a href="https://docs.switchagents.ai/switch-rooms/using/work-in-a-room" rel="noopener noreferrer"&gt;Work in a room&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Room commands&lt;/strong&gt; like &lt;code&gt;!set-alias @bull-researcher @bull&lt;/code&gt; handle housekeeping. (&lt;a href="https://docs.switchagents.ai/switch-rooms/resources/room-commands" rel="noopener noreferrer"&gt;Room commands&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One fact from the docs shaped my whole design: &lt;em&gt;follow-ups start only from a person's message, never from an agent's.&lt;/em&gt; I'll come back to it, because it is why this project is a human-led council and not an autonomous swarm.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture, in one picture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx24mjpx789z4ok1v9fku.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx24mjpx789z4ok1v9fku.png" alt="Investor Council architecture: terminal-only human CLI, Switch room, two Claude Code agents, restricted council-agent CLI, MySQL and frozen SEC snapshots" width="636" height="1020"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Architecture: the raw portfolio stays in the terminal-only box; agents reach MySQL only through the restricted &lt;code&gt;council-agent&lt;/code&gt; CLI.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Read it from top to bottom:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The human&lt;/strong&gt; works in two places: a terminal (&lt;code&gt;council human …&lt;/code&gt;) and the Mattermost room.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch&lt;/strong&gt; supplies the shared room, the addressable agents, the instructions every agent reads, and the audit trail of who said what.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agents&lt;/strong&gt; do not reason over free text alone. They call a small Python CLI, &lt;code&gt;./council-agent&lt;/code&gt;, which reads frozen SEC snapshots, verifies evidence and writes typed claims to &lt;strong&gt;MySQL&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The raw portfolio never leaves the terminal-only box.&lt;/strong&gt; The room sees only a sanitised summary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The principle I kept repeating while building it: &lt;strong&gt;chat carries discourse and pointers; the ledger carries structured truth.&lt;/strong&gt; Switch is the collaboration layer, not my orchestrator, and the database is the source of record.&lt;/p&gt;
&lt;h2&gt;
  
  
  The cast
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Bull Researcher
&lt;/h3&gt;

&lt;p&gt;Builds the strongest &lt;em&gt;evidence-backed&lt;/em&gt; positive case. Its instructions (&lt;a href="https://github.com/Krrish-agrawal/Investor-Council/blob/main/switch/role-bull.md" rel="noopener noreferrer"&gt;role-bull.md&lt;/a&gt;) tell it to work outward from the income statement ("what is working, and is it durable?"), put cash conversion first among its quality signals, and file anything the data cannot support (catalysts, market share, management quality) as an &lt;code&gt;ASSUMPTION&lt;/code&gt; with a falsifier or as a &lt;code&gt;GAP&lt;/code&gt;, never as a fact. Conceding is framed as a strength.&lt;/p&gt;
&lt;h3&gt;
  
  
  Bear Researcher
&lt;/h3&gt;

&lt;p&gt;Investigates weaknesses and risks, with a deliberately &lt;em&gt;different method&lt;/em&gt;: data integrity first (gaps, stale flags, conflicting values), then leverage, earnings erosion, cash and dilution, then portfolio exposure (&lt;a href="https://github.com/Krrish-agrawal/Investor-Council/blob/main/switch/role-bear.md" rel="noopener noreferrer"&gt;role-bear.md&lt;/a&gt;). The rule I care most about: &lt;strong&gt;it must not manufacture negatives.&lt;/strong&gt; "No material contrary evidence found" is a valid, first-class result, because a Bear that always finds a catastrophe is just a different kind of cheerleader.&lt;/p&gt;
&lt;h3&gt;
  
  
  The human
&lt;/h3&gt;

&lt;p&gt;By design the human is not a spectator. They start every stage, decide what goes in the room, rule on every flagged claim, and own the decision field in the dossier. The system is built so that it &lt;em&gt;cannot&lt;/em&gt; finish without them (in the S-0004 verification run, the test operator stood in for the human): &lt;code&gt;council human finalize&lt;/code&gt; refuses while any flagged claim lacks an &lt;code&gt;accept&lt;/code&gt; or &lt;code&gt;reject&lt;/code&gt; ruling, with no override flag.&lt;/p&gt;
&lt;h2&gt;
  
  
  Claims, evidence and the ruling gate
&lt;/h2&gt;

&lt;p&gt;This is the part of the project that has nothing to do with Switch and everything to do with whether the output is trustworthy.&lt;/p&gt;

&lt;p&gt;Every statement an agent files is a &lt;strong&gt;claim&lt;/strong&gt; with one of four types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FACT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Needs at least one evidence reference. With none, it is rejected. If the evidence is cited but doesn't verify, the claim is stored as &lt;code&gt;UNVERIFIED&lt;/code&gt; with &lt;code&gt;downgraded = true&lt;/code&gt;. It is never kept as a &lt;code&gt;FACT&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ASSUMPTION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An inference or forecast. Requires a &lt;code&gt;basis&lt;/code&gt; and a &lt;code&gt;falsifier&lt;/code&gt; (what would prove it wrong).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UNVERIFIED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Plausible but without verified evidence. Reported separately, never under "agreed facts".&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GAP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Needed data is absent. Never filled in. If an agent asks for a metric the snapshot lacks, a &lt;code&gt;GAP&lt;/code&gt; claim is created automatically, naming the XBRL concepts that were tried.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Evidence is not text the agent typed. &lt;code&gt;./council-agent evidence add S-0004 --metric revenue --basis annual&lt;/code&gt; makes the CLI look up the figure in a hash-checked snapshot and record the concept, unit, period, form, filing date, accession number, source URL and retrieval time. A claimed value that doesn't match the snapshot is stored as &lt;code&gt;FAILED&lt;/code&gt;, not silently corrected. Evidence also gets &lt;code&gt;STALE&lt;/code&gt; or &lt;code&gt;CONFLICT&lt;/code&gt; flags (a restated figure appearing in different filings) from thresholds in &lt;a href="https://github.com/Krrish-agrawal/Investor-Council/blob/main/config/freshness.yaml" rel="noopener noreferrer"&gt;config/freshness.yaml&lt;/a&gt;, which are labelled everywhere as &lt;em&gt;application heuristics, not accounting or regulatory standards&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Then comes &lt;strong&gt;the ruling gate&lt;/strong&gt;. A claim is &lt;em&gt;flagged&lt;/em&gt; if it is &lt;code&gt;UNVERIFIED&lt;/code&gt;, if it cites evidence that is unverified or flagged &lt;code&gt;STALE&lt;/code&gt;/&lt;code&gt;CONFLICT&lt;/code&gt;, or if the other agent disputed it or asked for evidence. A flagged claim is resolved only by the human's &lt;code&gt;accept&lt;/code&gt; or &lt;code&gt;reject&lt;/code&gt; (&lt;code&gt;needs_evidence&lt;/code&gt; does not count). Until then it shows up under &lt;strong&gt;UNRESOLVED&lt;/strong&gt; in a draft dossier, never among the findings, and &lt;code&gt;finalize&lt;/code&gt; refuses. The renderer, &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;finalize&lt;/code&gt; all call the same module (&lt;a href="https://github.com/Krrish-agrawal/Investor-Council/blob/main/council/report/gate.py" rel="noopener noreferrer"&gt;council/report/gate.py&lt;/a&gt;), so they cannot disagree about what is open.&lt;/p&gt;

&lt;p&gt;One more design choice: &lt;strong&gt;the dossier body is rendered by deterministic Python, not written by an LLM.&lt;/strong&gt; There is nothing in it to hallucinate. Agent-written content arrives only as claims, challenges and final positions, each labelled with who filed it.&lt;/p&gt;
&lt;h2&gt;
  
  
  How a session runs in Mattermost
&lt;/h2&gt;

&lt;p&gt;Every stage starts with a human message. The human generates it in the terminal and pastes it into the room (in S-0004 the test operator did the pasting through browser automation):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;council human session new &lt;span class="nt"&gt;--ticker&lt;/span&gt; SYNT &lt;span class="nt"&gt;--cik&lt;/span&gt; 9999999 &lt;span class="nt"&gt;--target-sector&lt;/span&gt; Technology &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--thesis&lt;/span&gt; &lt;span class="s2"&gt;"Is SYNTHETIC TEST CORP a durable compounder, and would adding it deepen my technology concentration?"&lt;/span&gt;
council human message S-0004 &lt;span class="nt"&gt;--stage&lt;/span&gt; 1 &lt;span class="nt"&gt;--copy&lt;/span&gt;     &lt;span class="c"&gt;# puts the stage message on the clipboard&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Stage 1: independent research
&lt;/h3&gt;

&lt;p&gt;The message tags both agents, carries &lt;code&gt;[session S-0004]&lt;/code&gt; and the sanitised portfolio summary, and tells each to run &lt;code&gt;./council-agent next S-0004&lt;/code&gt;, research independently, file claims, submit, and post a ~120-word brief. In S-0004, Switch started a session for each ("Starting a session to handle this"), both replied within a minute, and they worked &lt;strong&gt;concurrently&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa4r1ewbcm4aa0iobls8d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa4r1ewbcm4aa0iobls8d.png" alt="Bull agent whoami" width="800" height="438"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The first smoke test: &lt;code&gt;@bull-researcher&lt;/code&gt; was asked to run &lt;code&gt;./council-agent whoami&lt;/code&gt; from its working directory. The reply shows &lt;code&gt;"role": "bull"&lt;/code&gt; and &lt;code&gt;"db_user": "council_agent"&lt;/code&gt;: the agent is wired to its own folder and the restricted database account.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqgxi6htsa8vv4sikg9nc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqgxi6htsa8vv4sikg9nc.png" alt="Stage 1 briefs" width="799" height="571"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Stage 1 in the &lt;code&gt;investor-council&lt;/code&gt; room. The pasted message ends with the sanitised summary JSON. Below it, &lt;code&gt;bear-researcher&lt;/code&gt; and &lt;code&gt;bull-researcher&lt;/code&gt; post briefs citing claim codes (C-X9, C-B8…) and flagging that every registered figure is &lt;code&gt;STALE&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Stage 2: the challenge round
&lt;/h3&gt;

&lt;p&gt;The human runs &lt;code&gt;council human status&lt;/code&gt; (are both submissions in?), then &lt;code&gt;council human advance S-0004 --to 2&lt;/code&gt;, then pastes the stage-2 message. Now &lt;code&gt;./council-agent brief show --other&lt;/code&gt; unlocks, but only at stage 2 and only after the agent has filed its own brief. Each agent may challenge up to three of the other's claims, restating the claim fairly first and choosing a stance: &lt;code&gt;disputes&lt;/code&gt;, &lt;code&gt;partly_agrees&lt;/code&gt;, &lt;code&gt;agrees&lt;/code&gt; or &lt;code&gt;needs_evidence&lt;/code&gt;. A &lt;code&gt;disputes&lt;/code&gt; without counter-evidence is automatically converted to &lt;code&gt;needs_evidence&lt;/code&gt;. One round only; the CLI refuses a second.&lt;/p&gt;

&lt;p&gt;In S-0004 Bear filed three challenges and Bull filed two. Neither side found counter-evidence to dispute outright, and the transcript shows something I find more convincing than a staged fight: the Bear challenged Bull's "~12.5% net margin" claim by pointing out that without gross profit or operating income in the snapshot, the margin could be a below-the-line artefact, and Bull's final position &lt;em&gt;conceded&lt;/em&gt; that point.&lt;/p&gt;
&lt;h3&gt;
  
  
  Human rulings
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;council human status&lt;/code&gt; lists every flagged claim and why. A ruling is one command each. These are the actual test-operator rulings from S-0004:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;council human rule S-0004 C-B8 accept &lt;span class="nt"&gt;--note&lt;/span&gt; &lt;span class="s2"&gt;"Accepted as a statement of what the frozen snapshot shows (STALE flag noted). TEST RUN ruling on synthetic data."&lt;/span&gt;
council human rule S-0004 C-B13 reject &lt;span class="nt"&gt;--note&lt;/span&gt; &lt;span class="s2"&gt;"TEST RUN ruling on synthetic data; not an investment decision"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test operator rejected one claim: Bull's C-B13, which asserted a sector classification as if it were EDGAR evidence. Bull's final position then conceded it as "an evidentiary overreach I should not have filed." That is the loop the design is for: the human rules, the agent revises. (A real investor's rulings would of course be their own judgement. These show the mechanism, not a decision.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw4s44scoodin1wbeuuo5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw4s44scoodin1wbeuuo5.png" alt="Dossier: flagged claims accepted or rejected" width="800" height="520"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The completed dossier's argument section, rendered from the committed &lt;a href="https://github.com/Krrish-agrawal/Investor-Council/blob/main/docs/examples/dossier_S-0004.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/examples/dossier_S-0004.md&lt;/code&gt;&lt;/a&gt;: each flagged claim shows why it needed a ruling, the evidence and its &lt;code&gt;STALE&lt;/code&gt; flag, and the ruling note. This is a rendering of the Markdown file, not a Switch screenshot.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Stage 3: final positions and the dossier
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;advance --to 3&lt;/code&gt;, paste the stage-3 message, and each agent files concessions, what it still holds and what it could not establish. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;council human finalize S-0004 &lt;span class="nt"&gt;--note&lt;/span&gt; &lt;span class="s2"&gt;"your own words"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; demo_output/dossier_S-0004.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a ten-section Markdown dossier: portfolio overview, sector allocation, concentration, positive arguments, negative arguments, verified evidence (with accession number, filing date and source URL for every row), missing information, limitations, sources, and a decision field that the tool never fills. It carries a &lt;code&gt;FINAL&lt;/code&gt; banner, a &lt;code&gt;FROZEN SNAPSHOT&lt;/code&gt; banner and an advice-language lint pass. The full S-0004 dossier is committed at &lt;a href="https://github.com/Krrish-agrawal/Investor-Council/blob/main/docs/examples/dossier_S-0004.md" rel="noopener noreferrer"&gt;docs/examples/dossier_S-0004.md&lt;/a&gt; (a copy of the generated &lt;code&gt;demo_output/dossier_S-0004.md&lt;/code&gt;, which is git-ignored).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifkg8lzeh318z9rc2jgt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifkg8lzeh318z9rc2jgt.png" alt="Dossier header and portfolio overview" width="800" height="572"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The top of the FINAL dossier: the not-advice disclaimer, the &lt;code&gt;FINAL&lt;/code&gt; and &lt;code&gt;FROZEN SNAPSHOT&lt;/code&gt; banners, the verbatim thesis, and the sanitised portfolio overview (rounded weights only). Rendered from the Markdown file.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What did the council actually conclude? Not much, and that is the honest result. The synthetic snapshot has one annual period and one quarter, with no gross profit, operating income, cash flow, debt or equity. Both agents ended with "could not establish a durable-compounder thesis" and a long list of &lt;code&gt;GAP&lt;/code&gt; claims (eight missing metrics each). Every evidence row is flagged &lt;code&gt;STALE&lt;/code&gt; (674 days against a 456-day heuristic). I'd rather ship a council that says "the data can't support this" than one that confidently fills the hole.&lt;/p&gt;
&lt;h2&gt;
  
  
  The supporting pieces
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The Python CLI
&lt;/h3&gt;

&lt;p&gt;Two entry points (&lt;a href="https://github.com/Krrish-agrawal/Investor-Council/blob/main/pyproject.toml" rel="noopener noreferrer"&gt;pyproject.toml&lt;/a&gt;): &lt;code&gt;council&lt;/code&gt; (human) and &lt;code&gt;council-agent&lt;/code&gt; (agent). The split is the security boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;council human …&lt;/code&gt; is what the human runs: &lt;code&gt;ingest&lt;/code&gt;, &lt;code&gt;summary&lt;/code&gt;, &lt;code&gt;session new&lt;/code&gt;, &lt;code&gt;message&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;advance&lt;/code&gt;, &lt;code&gt;rule&lt;/code&gt;, &lt;code&gt;finalize&lt;/code&gt;, &lt;code&gt;report&lt;/code&gt;, &lt;code&gt;data fetch/register/inspect/manifest&lt;/code&gt;, &lt;code&gt;agents init&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;./council-agent …&lt;/code&gt; is what the agents run: &lt;code&gt;next&lt;/code&gt;, &lt;code&gt;data show&lt;/code&gt;, &lt;code&gt;evidence add&lt;/code&gt;, &lt;code&gt;claim add&lt;/code&gt;, &lt;code&gt;submit&lt;/code&gt;, &lt;code&gt;brief show --other&lt;/code&gt;, &lt;code&gt;challenge add&lt;/code&gt;, &lt;code&gt;final&lt;/code&gt;, &lt;code&gt;whoami&lt;/code&gt;. Output is JSON. The agent's role comes &lt;strong&gt;only&lt;/strong&gt; from its directory's &lt;code&gt;.council.env&lt;/code&gt;, never from an argument, so Bull cannot pass &lt;code&gt;--as bear&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;council human agents init&lt;/code&gt; generates both agent directories: a restricted &lt;code&gt;.council.env&lt;/code&gt;, a &lt;code&gt;council-agent&lt;/code&gt; wrapper script and a &lt;code&gt;CLAUDE.md&lt;/code&gt;. The same text is also pasted into Switch's &lt;strong&gt;Agent instructions&lt;/strong&gt; field, so it doesn't matter whether Console-launched Claude Code loads &lt;code&gt;CLAUDE.md&lt;/code&gt; on its own.&lt;/p&gt;
&lt;h3&gt;
  
  
  MySQL
&lt;/h3&gt;

&lt;p&gt;MySQL 8 in Docker Compose holds ten domain tables (portfolios, holdings, sessions, sources, evidence, claims, claim-evidence links, submissions, challenges, dossiers) plus a migrations table. Schema constraints do real work: unique keys allow one submission per agent per stage and one challenge per agent per target claim. The port is published on &lt;code&gt;127.0.0.1&lt;/code&gt; only, and defaults to &lt;strong&gt;3307&lt;/strong&gt; so it won't fight an existing MySQL on 3306.&lt;/p&gt;
&lt;h3&gt;
  
  
  SEC EDGAR and the frozen snapshot
&lt;/h3&gt;

&lt;p&gt;The adapter fetches company facts and filing metadata from &lt;code&gt;data.sec.gov&lt;/code&gt;, rate-limited (default 5 requests/second, never above the SEC's 10), requires &lt;em&gt;your own&lt;/em&gt; contact string as the User-Agent (there is deliberately no default), validates responses before caching, and refuses a cached file whose SHA-256 no longer matches its sidecar. Each run is labelled &lt;code&gt;FROZEN_SNAPSHOT&lt;/code&gt;: data from a past retrieval date, "newer filings may exist". EDGAR is used for financial statements only, so &lt;strong&gt;there is no stock-price data&lt;/strong&gt; and any valuation claim is a &lt;code&gt;GAP&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Security and portfolio-data isolation
&lt;/h2&gt;

&lt;p&gt;The most sensitive thing in this project is a person's holdings. Switch's docs are clear that every agent in a room can read the room's conversation, whoever is allowed to address it (&lt;a href="https://docs.switchagents.ai/switch-rooms/resources/working-safely-with-agents" rel="noopener noreferrer"&gt;Working safely with agents&lt;/a&gt;), and that documents in a room are available to all its members. So attaching my CSV "for a Portfolio Analyst agent" would expose it to every room member and agent, and to the model provider.&lt;/p&gt;

&lt;p&gt;What I did instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The CSV is parsed locally by Python&lt;/strong&gt; and never posted, attached or placed in an agent directory. Files with credential-like or account-number columns are refused; cost basis is ignored and never stored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portfolio math is deterministic Python&lt;/strong&gt;, not an LLM: weights, sector allocation, concentration (top-1/3/5, HHI, effective holdings) and same-sector overlap. A third "Portfolio Analyst" agent would have needed the holdings, so I didn't build one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The room sees a sanitised summary:&lt;/strong&gt; rounded percentages, counts, the target ticker and the price date. No shares, values, cost basis, other tickers or file fingerprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agents' database account can't see the private tables.&lt;/strong&gt; &lt;code&gt;council_agent&lt;/code&gt; has no &lt;code&gt;SELECT&lt;/code&gt; on &lt;code&gt;portfolios&lt;/code&gt;, &lt;code&gt;holdings&lt;/code&gt; or &lt;code&gt;dossiers&lt;/code&gt;, column-limited access on sessions and claims, and no &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt; anywhere (&lt;a href="https://github.com/Krrish-agrawal/Investor-Council/blob/main/council/db/grants.py" rel="noopener noreferrer"&gt;council/db/grants.py&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A leak test&lt;/strong&gt; derives every private value (tickers, shares, prices, market values, total) from the holdings and asserts none appears in the shareable summary or dossier, with a control proving the check can actually detect a leak. After the live run, the whole room transcript (33 posts including threads, pulled via the Mattermost API) and the FINAL dossier were checked for the seven private tickers and their numbers. None appeared.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What this does &lt;em&gt;not&lt;/em&gt; give you.&lt;/strong&gt; The agents run as my OS user and inherit my permissions. A Claude Code session with shell access could in principle read any file I can, including my human database credentials in &lt;code&gt;.env&lt;/code&gt;. The grants, directory separation and instructions are &lt;strong&gt;defence in depth, not a hard boundary&lt;/strong&gt;. So I demo only on a synthetic portfolio. And the sanitised summary and the thesis I type are still visible to room members and the model provider; rounded weights are still personal information.&lt;/p&gt;
&lt;h2&gt;
  
  
  Implementation challenges (the real ones)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Switch Console exited at launch with no window.&lt;/strong&gt; The shell environment exported &lt;code&gt;ELECTRON_RUN_AS_NODE&lt;/code&gt;, which makes Electron apps run as plain Node and quit silently. Fix: &lt;a href="https://github.com/Krrish-agrawal/Investor-Council/blob/main/scripts/start_switch.sh" rel="noopener noreferrer"&gt;scripts/start_switch.sh&lt;/a&gt; launches Console with &lt;code&gt;env -i&lt;/code&gt; and a minimal, explicit environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A corrupt installer.&lt;/strong&gt; The &lt;code&gt;.dmg&lt;/code&gt; that had been downloaded was not a valid disk image. The official release was fetched again and its SHA-256 verified before installing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Mattermost stopped when Docker restarted and Console didn't bring it back.&lt;/strong&gt; Starting Docker Desktop also restarts unrelated containers, so my launch script now checks the Mattermost container and starts and health-waits it if needed, without touching anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A MySQL was already running on port 3306 on my machine.&lt;/strong&gt; Compose publishes the project's MySQL on 3307. Port conflicts were just the first of several environment gotchas; Python 3.13 is also required and pinned (&lt;code&gt;&amp;gt;=3.13,&amp;lt;3.14&lt;/code&gt;), because nothing else was tested.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. How do agents run a CLI with a fixed environment?&lt;/strong&gt; The docs say agent processes get a fixed set of environment variables, so I couldn't pass database credentials through the environment. The answer is a &lt;code&gt;council-agent&lt;/code&gt; wrapper in each agent's directory plus a git-ignored &lt;code&gt;.council.env&lt;/code&gt; holding only the restricted account. The &lt;code&gt;whoami&lt;/code&gt; screenshot above is the proof: it works, from the agent's own working directory, as &lt;code&gt;council_agent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Independence is only partly enforceable.&lt;/strong&gt; Every agent in a room reads every message. I protect the blind first pass with three layers: parallel sessions, an instruction to file before reading, and a CLI that refuses &lt;code&gt;brief show --other&lt;/code&gt; until stage 2. That reduces cross-contamination; it does not eliminate it, and I say so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Hallucinated numbers.&lt;/strong&gt; The fix is structural: a &lt;code&gt;FACT&lt;/code&gt; can only cite evidence the CLI verified against a hash-checked snapshot, and the dossier is rendered by code. Agents still occasionally overreach in &lt;em&gt;framing&lt;/em&gt; (Bull's rejected C-B13), which is exactly what the ruling gate exists to catch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. "Stages need an agent-to-agent trigger" was the wrong design.&lt;/strong&gt; My first design leaned on agent-triggered handoffs, and my v0.2 rewrite removed them. The Switch docs say follow-ups start only from a person's message, and don't say what an agent mention does, so I stopped assuming it works. Making the human the trigger for every stage turned out to be a feature: it &lt;em&gt;is&lt;/em&gt; the governance model.&lt;/p&gt;
&lt;h2&gt;
  
  
  The limitation I have to be upfront about: manual orchestration (and what I could not verify about custom agent registration)
&lt;/h2&gt;

&lt;p&gt;One more piece I wanted was a non-LLM &lt;strong&gt;"Clerk"&lt;/strong&gt; agent that would run &lt;code&gt;advance&lt;/code&gt;, &lt;code&gt;rule&lt;/code&gt; and &lt;code&gt;finalize&lt;/code&gt; from the room instead of the terminal, so the room would feel like a complete workflow. &lt;strong&gt;I did not build it.&lt;/strong&gt; Here is exactly what I know and what I don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the docs say.&lt;/strong&gt; An agent that runs &lt;em&gt;outside&lt;/em&gt; Switch Console, "built with its own client", registers with a &lt;strong&gt;registration key&lt;/strong&gt;, and "anyone signed in to the Gateway can create a key" under &lt;strong&gt;API Keys&lt;/strong&gt; (&lt;a href="https://docs.switchagents.ai/switch-rooms/administer/people-and-agents#register-an-agent-that-runs-outside-switch-console" rel="noopener noreferrer"&gt;People and agents&lt;/a&gt;). Agents registered through Console are different: Console registers them with your signed-in session, so there is no key to mint (&lt;a href="https://docs.switchagents.ai/switch-rooms/internals/connectors-and-runtime#registration-and-credentials" rel="noopener noreferrer"&gt;Connectors and runtime&lt;/a&gt;). The Gateway is the browser dashboard for a server (&lt;a href="https://docs.switchagents.ai/switch-rooms/resources/glossary#gateway" rel="noopener noreferrer"&gt;glossary&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I checked.&lt;/strong&gt; On Nov 6 I looked, read-only, at the Switch Console 0.40.1 already open on my machine. Its &lt;strong&gt;Admin&lt;/strong&gt; area has three pages: &lt;em&gt;Connectors&lt;/em&gt;, &lt;em&gt;Status&lt;/em&gt; and &lt;em&gt;Connector apps&lt;/em&gt;. There is no API Keys page there. &lt;em&gt;Status&lt;/em&gt; shows my local server running (switch-core 0.31.0, "runs without Docker"), with only the Mattermost component stopped because Docker was off. I created no key and viewed no credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I could not verify.&lt;/strong&gt; I did not find the Gateway's own page on my local server, so I do not know whether &lt;strong&gt;API Keys → Create Registration Key&lt;/strong&gt; is available there. This is therefore &lt;strong&gt;a gap in what I found in Console's UI, not a demonstrated Switch platform limitation&lt;/strong&gt;. The docs say the capability exists. If you can reach your Gateway, try it before assuming my Clerk idea is blocked.&lt;/p&gt;

&lt;p&gt;The one alternative I considered, a Mattermost bot account, would mean creating a new credentialed identity. The permission check in my Claude Code session denied that and I did not work around it. I have not authorised it since.&lt;/p&gt;

&lt;p&gt;So here is what this means in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The orchestration is manual.&lt;/strong&gt; The human generates three stage messages with &lt;code&gt;council human message&lt;/code&gt; and pastes them into the room. &lt;code&gt;advance&lt;/code&gt;, &lt;code&gt;rule&lt;/code&gt; and &lt;code&gt;finalize&lt;/code&gt; are terminal commands. Nothing in the room advances a stage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agents do not coordinate with each other.&lt;/strong&gt; They respond to each other's &lt;em&gt;filed&lt;/em&gt; briefs because the human prompts them to, one round at a time, not through autonomous negotiation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I have not tested whether an agent can trigger another through a mention&lt;/strong&gt;, and the design deliberately never depends on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch did not orchestrate anything.&lt;/strong&gt; It provided the shared room, addressable agents with their own identities and directories, and the audit trail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would keep rulings and finalization as human-only steps even if a Clerk were possible, because that is the point of the project.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I tested, and what I did not
&lt;/h2&gt;

&lt;p&gt;From &lt;a href="https://github.com/Krrish-agrawal/Investor-Council/blob/main/docs/TEST_RESULTS.md" rel="noopener noreferrer"&gt;docs/TEST_RESULTS.md&lt;/a&gt; (Nov 5, 2026; macOS arm64, Python 3.13.12, Docker 29.9.2, MySQL 8.0.46, Switch Console 0.40.1 with switch-core 0.31.0, Claude Code 2.1.150):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Automated suite&lt;/td&gt;
&lt;td&gt;218 passed on Nov 5 with MySQL running (185 need no database, 33 use a real MySQL, including a scripted three-stage workflow). Re-checked Nov 6: 218 tests collected; with Docker off, 186 pass and 32 skip. The 33 MySQL tests were not re-run on Nov 6.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database security&lt;/td&gt;
&lt;td&gt;Agent account denied &lt;code&gt;SELECT&lt;/code&gt; on holdings/portfolios/dossiers and any &lt;code&gt;UPDATE&lt;/code&gt;/&lt;code&gt;DELETE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live Bull and Bear (S-0004)&lt;/td&gt;
&lt;td&gt;13 typed claims each; evidence verified; briefs posted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collaboration&lt;/td&gt;
&lt;td&gt;Parallel research; challenges filed (Bull 2, Bear 3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human governance&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;finalize&lt;/code&gt; refused while a claim was &lt;code&gt;needs_evidence&lt;/code&gt;; accepted after the &lt;strong&gt;test operator's&lt;/strong&gt; rulings. No run with a real person ruling is recorded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy&lt;/td&gt;
&lt;td&gt;No private tickers, shares, prices or values in the 33-post room transcript or the FINAL dossier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Live SEC EDGAR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not run.&lt;/strong&gt; Blocked on supplying my own contact string (an optional task I have not done yet); the adapter is validated on synthetic fixtures only, so real-response field names remain unconfirmed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Human steps from the room&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not built.&lt;/strong&gt; Manual orchestration; registration-key route unverified (see above)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data&lt;/td&gt;
&lt;td&gt;One synthetic company and one synthetic portfolio. The rulings in the published dossier are labelled test-run rulings, not investment decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;You need: macOS (the only platform I tested), &lt;strong&gt;Python 3.13&lt;/strong&gt;, Docker with Compose v2, a Claude Code account (&lt;code&gt;claude auth login&lt;/code&gt;), and Switch Console.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Krrish-agrawal/Investor-Council.git
&lt;span class="nb"&gt;cd &lt;/span&gt;Investor-Council

&lt;span class="c"&gt;# 1. Python environment&lt;/span&gt;
python3.13 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;".[dev]"&lt;/span&gt;

&lt;span class="c"&gt;# 2. Secrets: no defaults on purpose. Generate your own.&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="c"&gt;#   Set MYSQL_ROOT_PASSWORD and COUNCIL_DB_ADMIN_PASSWORD to the SAME value, plus&lt;/span&gt;
&lt;span class="c"&gt;#   COUNCIL_DB_HUMAN_PASSWORD and COUNCIL_DB_AGENT_PASSWORD, e.g.:&lt;/span&gt;
&lt;span class="c"&gt;#   openssl rand -base64 24 | tr -d '/+='&lt;/span&gt;

&lt;span class="c"&gt;# 3. Database, schema, accounts, agent folders, health check&lt;/span&gt;
scripts/start.sh

&lt;span class="c"&gt;# 4. Sanity check (database-free tests)&lt;/span&gt;
pytest &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"not db"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then Switch, which involves a few clicks no script can do for you (&lt;a href="https://github.com/Krrish-agrawal/Investor-Council/blob/main/switch/SETUP.md" rel="noopener noreferrer"&gt;switch/SETUP.md&lt;/a&gt; has the full table):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install and open &lt;a href="https://docs.switchagents.ai/switch-rooms/getting-started/install-switch-console" rel="noopener noreferrer"&gt;Switch Console&lt;/a&gt;. Under &lt;strong&gt;Settings → Agent providers&lt;/strong&gt;, Claude Code should read &lt;em&gt;Signed in&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a server → Run a server on this computer → Start.&lt;/strong&gt; The first start downloads a few GB.&lt;/li&gt;
&lt;li&gt;Register &lt;code&gt;bull-researcher&lt;/code&gt; and &lt;code&gt;bear-researcher&lt;/code&gt;: Run location &lt;em&gt;This computer&lt;/em&gt;, Directory &lt;code&gt;agents/&amp;lt;role&amp;gt;-researcher&lt;/code&gt; (absolute path), provider Claude Code, &lt;strong&gt;Auto mode on, Bypass permissions off&lt;/strong&gt;, and paste each agent's &lt;code&gt;CLAUDE.md&lt;/code&gt; into &lt;strong&gt;Agent instructions&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Create a Mattermost room named &lt;code&gt;investor-council&lt;/code&gt;, paste &lt;a href="https://github.com/Krrish-agrawal/Investor-Council/blob/main/switch/room-instructions.md" rel="noopener noreferrer"&gt;switch/room-instructions.md&lt;/a&gt; into &lt;strong&gt;Instructions&lt;/strong&gt;, add both agents, and run &lt;code&gt;!set-alias @bull-researcher @bull&lt;/code&gt; (and the same for bear).&lt;/li&gt;
&lt;li&gt;(macOS) &lt;code&gt;scripts/start_switch.sh&lt;/code&gt; starts Console safely and waits for Mattermost.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The demo (&lt;a href="https://github.com/Krrish-agrawal/Investor-Council/blob/main/switch/DEMO.md" rel="noopener noreferrer"&gt;switch/DEMO.md&lt;/a&gt;, about 10 minutes, all synthetic):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;.venv/bin/python scripts/seed_synthetic_snapshot.py             &lt;span class="c"&gt;# offline fixture company, CIK 9999999&lt;/span&gt;
.venv/bin/council human data register &lt;span class="nt"&gt;--cik&lt;/span&gt; 9999999
.venv/bin/council human ingest &lt;span class="nt"&gt;--file&lt;/span&gt; samples/portfolio.csv &lt;span class="nt"&gt;--name&lt;/span&gt; demo &lt;span class="nt"&gt;--sample&lt;/span&gt; &lt;span class="nt"&gt;--price-as-of&lt;/span&gt; 2026-10-30
.venv/bin/council human session new &lt;span class="nt"&gt;--ticker&lt;/span&gt; SYNT &lt;span class="nt"&gt;--cik&lt;/span&gt; 9999999 &lt;span class="nt"&gt;--target-sector&lt;/span&gt; Technology &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--thesis&lt;/span&gt; &lt;span class="s2"&gt;"Is SYNTHETIC TEST CORP a durable compounder, and would adding it deepen my technology concentration?"&lt;/span&gt;
&lt;span class="nv"&gt;S&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;S-000N                                                         &lt;span class="c"&gt;# the code printed above&lt;/span&gt;

.venv/bin/council human message &lt;span class="nv"&gt;$S&lt;/span&gt; &lt;span class="nt"&gt;--stage&lt;/span&gt; 1 &lt;span class="nt"&gt;--copy&lt;/span&gt;              &lt;span class="c"&gt;# paste into the room&lt;/span&gt;
.venv/bin/council human status &lt;span class="nv"&gt;$S&lt;/span&gt;                                &lt;span class="c"&gt;# wait for both submissions&lt;/span&gt;
.venv/bin/council human advance &lt;span class="nv"&gt;$S&lt;/span&gt; &lt;span class="nt"&gt;--to&lt;/span&gt; 2
.venv/bin/council human message &lt;span class="nv"&gt;$S&lt;/span&gt; &lt;span class="nt"&gt;--stage&lt;/span&gt; 2 &lt;span class="nt"&gt;--copy&lt;/span&gt;              &lt;span class="c"&gt;# paste into the room&lt;/span&gt;
.venv/bin/council human status &lt;span class="nv"&gt;$S&lt;/span&gt;                                &lt;span class="c"&gt;# flagged claims and why&lt;/span&gt;
.venv/bin/council human rule &lt;span class="nv"&gt;$S&lt;/span&gt; C-B8 accept &lt;span class="nt"&gt;--note&lt;/span&gt; &lt;span class="s2"&gt;"..."&lt;/span&gt;         &lt;span class="c"&gt;# accept or reject each one&lt;/span&gt;
.venv/bin/council human advance &lt;span class="nv"&gt;$S&lt;/span&gt; &lt;span class="nt"&gt;--to&lt;/span&gt; 3
.venv/bin/council human message &lt;span class="nv"&gt;$S&lt;/span&gt; &lt;span class="nt"&gt;--stage&lt;/span&gt; 3 &lt;span class="nt"&gt;--copy&lt;/span&gt;              &lt;span class="c"&gt;# paste into the room&lt;/span&gt;
.venv/bin/council human finalize &lt;span class="nv"&gt;$S&lt;/span&gt; &lt;span class="nt"&gt;--note&lt;/span&gt; &lt;span class="s2"&gt;"your own words"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; dossier_&lt;span class="nv"&gt;$S&lt;/span&gt;.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two caveats: claim codes (like &lt;code&gt;C-B8&lt;/code&gt;) depend on what your agents file, so use the ones &lt;code&gt;status&lt;/code&gt; prints, and the first stage takes about two minutes. No Switch? &lt;code&gt;scripts/demo.sh --auto&lt;/code&gt; runs the same flow with two headless Claude Code sessions and stand-in rulings, but it only exists as a fallback, and I did not re-run it for this post.&lt;/p&gt;

&lt;p&gt;For real data, set &lt;code&gt;COUNCIL_SEC_USER_AGENT="Your Name you@example.com"&lt;/code&gt; (your own contact, never someone else's), then &lt;code&gt;council human data fetch --cik &amp;lt;CIK&amp;gt;&lt;/code&gt; and &lt;code&gt;council human data register --cik &amp;lt;CIK&amp;gt;&lt;/code&gt;. As noted above, I have not run this path against the live SEC.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned, and advice for first-time Switch users
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Design for the human trigger, not against it.&lt;/strong&gt; Switch is built around people addressing agents. When I stopped trying to chain agents together and made each stage a human message, the architecture got simpler and the governance story got stronger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put structure in a ledger; keep the room for discourse.&lt;/strong&gt; A session might not have prior room history (new session, reset, compaction), so every stage message carries &lt;code&gt;[session S-0004]&lt;/code&gt; and agents re-hydrate state from &lt;code&gt;./council-agent next&lt;/code&gt;. "Trust the tool over chat history" is in the agent instructions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write Room instructions like a protocol, and Agent instructions like a role contract.&lt;/strong&gt; Short, numbered, with explicit "do not" lines and "report errors plainly instead of producing plausible output". The docs' guidance on &lt;a href="https://docs.switchagents.ai/switch-rooms/using/shared-context" rel="noopener noreferrer"&gt;shared context&lt;/a&gt; is worth reading first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose names and directories before you create agents.&lt;/strong&gt; Both are fixed at creation, and re-registering means a new identity and re-inviting. I decided on &lt;code&gt;bull-researcher&lt;/code&gt; and &lt;code&gt;bear-researcher&lt;/code&gt; up front for that reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find out what keeps your agent answering.&lt;/strong&gt; It depends on who runs it. If Switch Console runs the agent on your computer, quitting Console stops it. If a managed machine runs it (the default on a server Console runs, since Console 0.40), quitting Console has no effect, but the machine must be on and its agent controller running. Open &lt;strong&gt;Your Agents → the agent&lt;/strong&gt; to see which applies (&lt;a href="https://docs.switchagents.ai/switch-rooms/getting-started/what-keeps-your-agent-running" rel="noopener noreferrer"&gt;What keeps your agent running&lt;/a&gt;). If an agent is silent, check that first, then check you addressed it by its exact name (&lt;a href="https://docs.switchagents.ai/switch-rooms/using/agent-not-answering" rel="noopener noreferrer"&gt;When an agent doesn't answer&lt;/a&gt;). Either way, keep the machine awake during a run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with Auto mode on and Bypass off.&lt;/strong&gt; In S-0004 the agents ran &lt;code&gt;./council-agent&lt;/code&gt; without an approval stall. If an approval does appear it may be in a thread, not the main channel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag every agent explicitly, every time.&lt;/strong&gt; An &lt;code&gt;@&lt;/code&gt; always wins, and a message without one reaches no agent unless the server has follow-ups on. Follow-ups reach "the agent that last spoke, or was last tagged, in the thread", which is ambiguous with two agents, so every stage message in this project tags both agents by name. (&lt;code&gt;!follow-ups off&lt;/code&gt; turns them off per room.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume everything in a room is visible to every agent.&lt;/strong&gt; Decide what to share before you paste, not after. This one decision (no CSV in the room) shaped half of my architecture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the undocumented stuff early.&lt;/strong&gt; I wrote down every assumption (does &lt;code&gt;CLAUDE.md&lt;/code&gt; load? do two sessions run at once?) as a numbered test and ran them against the real app (all but the live-SEC one). Writing the fallback beside each assumption meant a failed test cost me a paragraph of docs, not a redesign.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write limitations down as you hit them, and re-check them before you publish.&lt;/strong&gt; The Clerk gap is in my test results from the day it happened, and my first note called it "blocked". Re-reading the docs and the Console showed the accurate statement is narrower: not found in Console's UI, unverified in the Gateway. A limitation you haven't re-checked is a claim you can't back up.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;What Switch gave me, concretely: a shared room the human and both agents could see, addressable agents with their own identities and directories, a way to interject at any moment, and an audit trail. What it did not give me: orchestration. The stage control and rulings live in my own CLI, and I kept the human steps manual on purpose, because the thing I wanted was not a pipeline of agents but a disciplined argument that a person gets to settle. The one piece of automation I wanted, a room-side Clerk agent, I did not build.&lt;/p&gt;

&lt;p&gt;If you want to try it, the repo is at &lt;a href="https://github.com/Krrish-agrawal/Investor-Council" rel="noopener noreferrer"&gt;github.com/Krrish-agrawal/Investor-Council&lt;/a&gt;. If you build something similar, I'd love to hear whether you can reach your Gateway's API Keys page on a local server and register an external agent. That's the piece that would turn my manual council into one that runs from the room.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Investor Council is a research tool, not an adviser. Nothing in this post or the project is a recommendation to buy, sell or hold any security.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Further reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.switchagents.ai/switch-rooms/getting-started/index" rel="noopener noreferrer"&gt;Switch Rooms: quickstart and getting started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.switchagents.ai/switch-rooms/using/how-switch-works" rel="noopener noreferrer"&gt;How Switch works&lt;/a&gt; and &lt;a href="https://docs.switchagents.ai/switch-rooms/internals/life-of-a-message" rel="noopener noreferrer"&gt;Life of a message&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.switchagents.ai/switch-rooms/internals/identity-and-access" rel="noopener noreferrer"&gt;Identity and access&lt;/a&gt; and &lt;a href="https://docs.switchagents.ai/switch-rooms/resources/working-safely-with-agents" rel="noopener noreferrer"&gt;Working safely with agents&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.switchagents.ai/switch-rooms/getting-started/what-keeps-your-agent-running" rel="noopener noreferrer"&gt;What keeps your agent running&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sec.gov/os/accessing-edgar-data" rel="noopener noreferrer"&gt;SEC EDGAR APIs and fair-access policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Project docs: &lt;a href="https://github.com/Krrish-agrawal/Investor-Council/blob/main/docs/ARCHITECTURE.md" rel="noopener noreferrer"&gt;ARCHITECTURE&lt;/a&gt; · &lt;a href="https://github.com/Krrish-agrawal/Investor-Council/blob/main/docs/AGENT_DESIGN.md" rel="noopener noreferrer"&gt;AGENT_DESIGN&lt;/a&gt; · &lt;a href="https://github.com/Krrish-agrawal/Investor-Council/blob/main/docs/TECHNICAL_RISKS.md" rel="noopener noreferrer"&gt;TECHNICAL_RISKS&lt;/a&gt; · &lt;a href="https://github.com/Krrish-agrawal/Investor-Council/blob/main/docs/TEST_RESULTS.md" rel="noopener noreferrer"&gt;TEST_RESULTS&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agenticai</category>
      <category>claudecode</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
