{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/mistral-ai/2026-03-31-spaces-a-cli-built-for-humans-and-agents/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "Most developer tools start the same way. You need to do a thing repeatedly, so you write a script. The script grows flags. The flags grow subcommands. Before you know it, you've got a CLI.\n\nWe had this exact experience when building an internal platform tool to help our solutions team ship faster. It scaffolds projects, spins up dev environments, generates configs, and deploys to staging.\n\nAs our CLI grew in scope, so did the number of our internal users. But then, something interesting happened: suddenly, we were no longer building just for human developers, but for coding agents too.\n\n## What Good DX Looks Like\n\nBefore we talk about agents, let's talk about what makes a CLI actually pleasant for human developers.\n\nSpaces is opinionated about the boring stuff. It picks sane directory structures so you don't have to. It generates the config files you'd otherwise copy from the last project. It wires services together so your API and your frontend can talk on day one, not after an hour of YAML editing or “vibing”.\n\nHere's what a typical session looks like:\n\n```text\n$ spaces init my-project$ cd my-project$ spaces dev\n```\n\nThree commands. You go from nothing to a running multi-service project with hot reload, a database, and generated Dockerfiles. That's the bar.\n\nThe commands that matter tend to fall into three buckets:\n\n- **Scaffolding** -- These create structure, ask questions, show options, let you explore.\n\n- **Development** -- These are your inner loop. They just “run”.\n\n- **Operations** -- These touch production. Careful, double-check before acting\n\nThis is all table stakes. But then our second user showed up and we discovered we needed something a little different, and a lot more interesting.\n\n## The Second User: The Agent\n\nWe built a TUI module picker for `init`. Fancy changes, commands, it looked super.\n\nThen an agent tried to use it.\n\nThe agent saw raw ANSI escape codes. `\\\\\\\\x1b[36m?\\\\\\\\x1b[0m Select components`. It couldn't send arrow keys or toggle selections. It was locked out of the command entirely.\n\nThe fix seems obvious in retrospect: add a `--components` flag. But the real insight was bigger than one flag.\n\n## Every prompt is a flag in disguise\n\nEvery time your CLI asks an interactive question, there's an implicit contract: \"I need this piece of information to proceed.\" To fulfill that contract, you can use an interactive prompt. Or you can use a flag or config file.\n\nThe trick is to think about the *information* first and the *input method* second.\n\n```python\ndef init_command(    components: str | None = Option(None),    yes: bool = Option(False, \"-y\"),):    if components:        selected = components.split(\",\")    elif yes:        selected = get_defaults()    else:        selected = show_picker()\n    # Same logic from here    create_project(selected)\n```\n\nThree input paths, one execution path. The business logic doesn't know how the inputs arrived. This means you test it once, not three times.\n\nThe `-y` flag deserves special attention. It's not just \"skip confirmations.\" It's a contract that says: *I'm providing everything you need programmatically, don't block on stdin.* When you run with `-y`, every prompt resolves to either a flag value or a smart default. If it can't, it fails loudly instead of hanging.\n\n**Example in practice: the interactive elements in this post.**\n\nThe elements were built in a fresh repository. An agent was retroactively asked to wire the project into \\[spaces cli\\] for deployment. It started by running `--help` across the relevant commands to understand the interface. From that, it figured out which configuration files were needed, generated a `config.yaml`, and connected the Dockerfile and registry settings - no hand-holding required.\n\nIn the same pass, it set up the GitHub Actions CI pipeline. From a single prompt to a live deployment took under 10 minutes (a cycle time we're actively working to bring down). After that, the repository was fully configured and the embeds were [deployed](https://spaces-blogs-staging-mistral-01d83c8e.koyeb.app/) with [Koyeb](https://www.koyeb.com/) — as a Space, scaffolded and deployed through Spaces itself. Yes, the interactive demos embedded in this blog post about Spaces are running as a Space. *Spaception*.\n\nBecause every interactive input had a flag equivalent, the agent could operate autonomously end-to-end.\n\n## Structured Data as Interface\n\nThe CLI our team was building helped our applied AI engineers ship apps. But not every app looks the same. Some need a backend and a vector database. Others just need a relational database, a frontend, and some APIs. A few are worker-only services with no UI at all.\n\nWe were not about to hardcode the module types. So we built a plugin system where each component is a plugin that declares its own properties:\n\n```python\nclass ModulePlugin(BaseModel):    type_id: str    category: str    default_port: int\n    def get_env_vars(self) -> list[EnvVarDef]: ...    def get_dev_command(self, port: int) -> str: ...\n```\n\nPlugins are introspectable. You can list them, serialize them, diff them. A human browses a TUI picker. An agent queries the registry and gets JSON back. Same data, different rendering.\n\nThis accidentally solved a problem we didn't know we had. Before, adding a new module type meant updating the picker, the Dockerfile generator, the env file writer, and the compose template. Now it means writing one plugin class. The registry is the single source of truth and everything reads from it.\n\n## Teaching Agents About Your Project\n\n> > There's an old saying that content is king. With agents, context is.\n\nThe most impactful thing we did for agent usability was generating two files on every `init`:\n\n`context.json` -- a structured snapshot of the project: what modules exist, ports they use, commands to run, env vars they need.\n\n`AGENTS.md` -- a set of rules written for LLMs and more imperative than your regular one. Not \"this project uses PostgreSQL\" but \"run `mycli dev --migrate` before testing database changes.\"\n\nAn agent that reads these files before acting makes dramatically fewer mistakes. Basically, it won’t guess at port numbers, run the wrong test command, try to install dependencies that are already managed by the toolchain.\n\nThe context file also acts as a cache-buster for stale agent assumptions. When you add a module or change a deploy target, the context file updates automatically on the next `dev` or `init`. The agent reads fresh state every time.\n\n## Implicit State is the Enemy\n\nThe subtlest problem we hit was implicit state. Our `add` command read `config.yaml` from the current working directory. A human `cd`s to the right folder without thinking. An agent running commands from a workspace root has no idea it needs to be in a subdirectory.\n\nThe fix:\n\n```python\n# Before: CWD dependencyconfig = load_config(Path.cwd() / \"config.yaml\")\n# After: explicit with fallbackconfig = load_config(    path or find_config_in_parents(Path.cwd()))\n```\n\nEvery hidden assumption -- CWD, environment variables, dotfiles in `$HOME` -- is a place where an agent will trip. Explicit parameters with sensible fallbacks solve it for agents and make scripting easier for humans too.\n\n## The Checklist\n\nLooking back, the changes were small individually. Just a set of principles applied consistently:\n\n- Every interactive input has a flag equivalent\n\n- Every flag has a smart default for headless mode\n\n- State is explicit. CWD, env vars, and config paths are inputs, not assumptions\n\n- Plugins are data models, not just code. Introspectable by default\n\n- Context files give agents (and CI, and scripts) a structured description of the project\n\n## Building a Better Tool for Everyone\n\nThe funny part is that none of this made the CLI worse for humans. The TUI picker still works and looks fancy, progress spinners still spin, confirmation dialogs still confirm. We just added a second door.\n\nAnd that second door turned out to be the more important one. Not because agents matter more than humans, but because the constraints they impose are the same constraints that make a CLI composable, scriptable, and testable. Designing for agents forced us to build a better tool for everyone.\n\nIf you're building developer tools right now, you don't need a separate agent API. You need to look at every `input()` call, every CWD assumption, every pretty-printed-only output, and ask: what if the user on the other end is a process, not a person?\n\nThe answer to that question will improve your tool either way.\n\nSpaces CLI is built by Lorenzo Signoretti, Riwa Hoteit & Sam Fenwick at Mistral AI. Special thanks to our Applied AI team, the earliest and most demanding users of the CLI, whose real-world usage shaped every pattern described here. We're excited to see the applications it will help them build with our Customers to solve painful use cases.\n\n**The tooling layer between humans and agents is still being figured out. If building developer tools at the intersection of AI and infrastructure sounds like your thing,** [**we’re hiring**](https://mistral.ai/careers)**.**\n",
  "authors": [],
  "captured_at": "2026-10-19T01:26:53.998049939+00:00",
  "category": "ai",
  "content_html": "<p>Most developer tools start the same way. You need to do a thing repeatedly, so you write a script. The script grows flags. The flags grow subcommands. Before you know it, you've got a CLI.</p>\n<p>We had this exact experience when building an internal platform tool to help our solutions team ship faster. It scaffolds projects, spins up dev environments, generates configs, and deploys to staging.</p>\n<p>As our CLI grew in scope, so did the number of our internal users. But then, something interesting happened: suddenly, we were no longer building just for human developers, but for coding agents too.</p>\n<h2>What Good DX Looks Like</h2>\n<p>Before we talk about agents, let's talk about what makes a CLI actually pleasant for human developers.</p>\n<p>Spaces is opinionated about the boring stuff. It picks sane directory structures so you don't have to. It generates the config files you'd otherwise copy from the last project. It wires services together so your API and your frontend can talk on day one, not after an hour of YAML editing or “vibing”.</p>\n<p>Here's what a typical session looks like:</p>\n<pre><code class=\"language-text\"><span class=\"code-snippet\">$ spaces init my-project$ cd my-project$ spaces dev\n</span></code></pre>\n<p>Three commands. You go from nothing to a running multi-service project with hot reload, a database, and generated Dockerfiles. That's the bar.</p>\n<p>The commands that matter tend to fall into three buckets:</p>\n<ul>\n<li>\n<p><strong>Scaffolding</strong> -- These create structure, ask questions, show options, let you explore.</p>\n</li>\n<li>\n<p><strong>Development</strong> -- These are your inner loop. They just “run”.</p>\n</li>\n<li>\n<p><strong>Operations</strong> -- These touch production. Careful, double-check before acting</p>\n</li>\n</ul>\n<p>This is all table stakes. But then our second user showed up and we discovered we needed something a little different, and a lot more interesting.</p>\n<h2>The Second User: The Agent</h2>\n<p>We built a TUI module picker for <code>init</code>. Fancy changes, commands, it looked super.</p>\n<p>Then an agent tried to use it.</p>\n<p>The agent saw raw ANSI escape codes. <code>\\\\\\\\x1b[36m?\\\\\\\\x1b[0m Select components</code>. It couldn't send arrow keys or toggle selections. It was locked out of the command entirely.</p>\n<p>The fix seems obvious in retrospect: add a <code>--components</code> flag. But the real insight was bigger than one flag.</p>\n<h2>Every prompt is a flag in disguise</h2>\n<p>Every time your CLI asks an interactive question, there's an implicit contract: &quot;I need this piece of information to proceed.&quot; To fulfill that contract, you can use an interactive prompt. Or you can use a flag or config file.</p>\n<p>The trick is to think about the <em>information</em> first and the <em>input method</em> second.</p>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-storage syntax-type syntax-function syntax-python\">def</span> <span class=\"syntax-entity syntax-name syntax-function syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">init_command</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-begin syntax-python\">(</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\">    <span class=\"syntax-variable syntax-parameter syntax-python\">components</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-annotation syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-parameter syntax-python\">:</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">str</span></span> <span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-python\">|</span> <span class=\"syntax-constant syntax-language syntax-python\">None</span> </span><span class=\"syntax-meta syntax-function syntax-parameters syntax-default-value syntax-python\"><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">Option</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-constant syntax-language syntax-python\">None</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span>    <span class=\"syntax-variable syntax-parameter syntax-python\">yes</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-annotation syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-parameter syntax-python\">:</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">bool</span></span> </span><span class=\"syntax-meta syntax-function syntax-parameters syntax-default-value syntax-python\"><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">Option</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-constant syntax-language syntax-python\">False</span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">-y<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-function syntax-begin syntax-python\">:</span></span>    <span class=\"syntax-meta syntax-statement syntax-if syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-conditional syntax-python\">if</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">components</span></span><span class=\"syntax-punctuation syntax-section syntax-block syntax-conditional syntax-python\">:</span></span>        <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">selected</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">components</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">split</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">,<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>    <span class=\"syntax-meta syntax-statement syntax-conditional syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-conditional syntax-python\">elif</span></span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">yes</span></span><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-variable syntax-python\">:</span>        <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">selected</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">get_defaults</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>    <span class=\"syntax-meta syntax-statement syntax-conditional syntax-python\"><span class=\"syntax-keyword syntax-control syntax-flow syntax-conditional syntax-python\">else</span><span class=\"syntax-punctuation syntax-section syntax-block syntax-python\">:</span></span>        <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">selected</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">show_picker</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n    <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Same logic from here    create_project(selected)\n</span></span></span></code></pre>\n<p>Three input paths, one execution path. The business logic doesn't know how the inputs arrived. This means you test it once, not three times.</p>\n<p>The <code>-y</code> flag deserves special attention. It's not just &quot;skip confirmations.&quot; It's a contract that says: <em>I'm providing everything you need programmatically, don't block on stdin.</em> When you run with <code>-y</code>, every prompt resolves to either a flag value or a smart default. If it can't, it fails loudly instead of hanging.</p>\n<p><strong>Example in practice: the interactive elements in this post.</strong></p>\n<p>The elements were built in a fresh repository. An agent was retroactively asked to wire the project into [spaces cli] for deployment. It started by running <code>--help</code> across the relevant commands to understand the interface. From that, it figured out which configuration files were needed, generated a <code>config.yaml</code>, and connected the Dockerfile and registry settings - no hand-holding required.</p>\n<p>In the same pass, it set up the GitHub Actions CI pipeline. From a single prompt to a live deployment took under 10 minutes (a cycle time we're actively working to bring down). After that, the repository was fully configured and the embeds were <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://spaces-blogs-staging-mistral-01d83c8e.koyeb.app/\">deployed</a> with <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.koyeb.com/\">Koyeb</a> — as a Space, scaffolded and deployed through Spaces itself. Yes, the interactive demos embedded in this blog post about Spaces are running as a Space. <em>Spaception</em>.</p>\n<p>Because every interactive input had a flag equivalent, the agent could operate autonomously end-to-end.</p>\n<h2>Structured Data as Interface</h2>\n<p>The CLI our team was building helped our applied AI engineers ship apps. But not every app looks the same. Some need a backend and a vector database. Others just need a relational database, a frontend, and some APIs. A few are worker-only services with no UI at all.</p>\n<p>We were not about to hardcode the module types. So we built a plugin system where each component is a plugin that declares its own properties:</p>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-meta syntax-class syntax-python\"><span class=\"syntax-storage syntax-type syntax-class syntax-python\">class</span> <span class=\"syntax-entity syntax-name syntax-class syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">ModulePlugin</span></span><span class=\"syntax-meta syntax-class syntax-inheritance syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-inheritance syntax-begin syntax-python\">(</span></span></span><span class=\"syntax-meta syntax-class syntax-inheritance syntax-python\"><span class=\"syntax-entity syntax-other syntax-inherited-class syntax-python\">BaseModel</span><span class=\"syntax-punctuation syntax-section syntax-inheritance syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-class syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-class syntax-begin syntax-python\">:</span></span>    <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">type_id</span></span><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-variable syntax-python\">:</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">str</span></span>    <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">category</span></span><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-variable syntax-python\">:</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">str</span></span>    <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">default_port</span></span><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-variable syntax-python\">:</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">int</span></span>\n<span class=\"syntax-meta syntax-function syntax-python\">    <span class=\"syntax-storage syntax-type syntax-function syntax-python\">def</span> <span class=\"syntax-entity syntax-name syntax-function syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">get_env_vars</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-begin syntax-python\">(</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-variable syntax-parameter syntax-python\">self</span><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function syntax-python\"> </span><span class=\"syntax-meta syntax-function syntax-annotation syntax-return syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-return syntax-python\">-&gt;</span> <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">list</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">EnvVarDef</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span></span><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-function syntax-begin syntax-python\">:</span></span> <span class=\"syntax-constant syntax-language syntax-python\">...</span>    <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-invalid syntax-illegal syntax-name syntax-python\">def</span></span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">get_dev_command</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">port</span></span>: <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">int</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span> <span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-python\">-</span><span class=\"syntax-keyword syntax-operator syntax-comparison syntax-python\">&gt;</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">str</span></span><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-variable syntax-python\">:</span> <span class=\"syntax-constant syntax-language syntax-python\">...</span>\n</span></span></code></pre>\n<p>Plugins are introspectable. You can list them, serialize them, diff them. A human browses a TUI picker. An agent queries the registry and gets JSON back. Same data, different rendering.</p>\n<p>This accidentally solved a problem we didn't know we had. Before, adding a new module type meant updating the picker, the Dockerfile generator, the env file writer, and the compose template. Now it means writing one plugin class. The registry is the single source of truth and everything reads from it.</p>\n<h2>Teaching Agents About Your Project</h2>\n<blockquote>\n<blockquote>\n<p>There's an old saying that content is king. With agents, context is.</p>\n</blockquote>\n</blockquote>\n<p>The most impactful thing we did for agent usability was generating two files on every <code>init</code>:</p>\n<p><code>context.json</code> -- a structured snapshot of the project: what modules exist, ports they use, commands to run, env vars they need.</p>\n<p><code>AGENTS.md</code> -- a set of rules written for LLMs and more imperative than your regular one. Not &quot;this project uses PostgreSQL&quot; but &quot;run <code>mycli dev --migrate</code> before testing database changes.&quot;</p>\n<p>An agent that reads these files before acting makes dramatically fewer mistakes. Basically, it won’t guess at port numbers, run the wrong test command, try to install dependencies that are already managed by the toolchain.</p>\n<p>The context file also acts as a cache-buster for stale agent assumptions. When you add a module or change a deploy target, the context file updates automatically on the next <code>dev</code> or <code>init</code>. The agent reads fresh state every time.</p>\n<h2>Implicit State is the Enemy</h2>\n<p>The subtlest problem we hit was implicit state. Our <code>add</code> command read <code>config.yaml</code> from the current working directory. A human <code>cd</code>s to the right folder without thinking. An agent running commands from a workspace root has no idea it needs to be in a subdirectory.</p>\n<p>The fix:</p>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> Before: CWD dependencyconfig = load_config(Path.cwd() / &quot;config.yaml&quot;)\n</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> After: explicit with fallbackconfig = load_config(    path or find_config_in_parents(Path.cwd()))\n</span></span></span></code></pre>\n<p>Every hidden assumption -- CWD, environment variables, dotfiles in <code>$HOME</code> -- is a place where an agent will trip. Explicit parameters with sensible fallbacks solve it for agents and make scripting easier for humans too.</p>\n<h2>The Checklist</h2>\n<p>Looking back, the changes were small individually. Just a set of principles applied consistently:</p>\n<ul>\n<li>\n<p>Every interactive input has a flag equivalent</p>\n</li>\n<li>\n<p>Every flag has a smart default for headless mode</p>\n</li>\n<li>\n<p>State is explicit. CWD, env vars, and config paths are inputs, not assumptions</p>\n</li>\n<li>\n<p>Plugins are data models, not just code. Introspectable by default</p>\n</li>\n<li>\n<p>Context files give agents (and CI, and scripts) a structured description of the project</p>\n</li>\n</ul>\n<h2>Building a Better Tool for Everyone</h2>\n<p>The funny part is that none of this made the CLI worse for humans. The TUI picker still works and looks fancy, progress spinners still spin, confirmation dialogs still confirm. We just added a second door.</p>\n<p>And that second door turned out to be the more important one. Not because agents matter more than humans, but because the constraints they impose are the same constraints that make a CLI composable, scriptable, and testable. Designing for agents forced us to build a better tool for everyone.</p>\n<p>If you're building developer tools right now, you don't need a separate agent API. You need to look at every <code>input()</code> call, every CWD assumption, every pretty-printed-only output, and ask: what if the user on the other end is a process, not a person?</p>\n<p>The answer to that question will improve your tool either way.</p>\n<p>Spaces CLI is built by Lorenzo Signoretti, Riwa Hoteit &amp; Sam Fenwick at Mistral AI. Special thanks to our Applied AI team, the earliest and most demanding users of the CLI, whose real-world usage shaped every pattern described here. We're excited to see the applications it will help them build with our Customers to solve painful use cases.</p>\n<p><strong>The tooling layer between humans and agents is still being figured out. If building developer tools at the intersection of AI and infrastructure sounds like your thing,</strong> <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://mistral.ai/careers\"><strong>we’re hiring</strong></a><strong>.</strong></p>\n",
  "content_markdown": "Most developer tools start the same way. You need to do a thing repeatedly, so you write a script. The script grows flags. The flags grow subcommands. Before you know it, you've got a CLI.\n\nWe had this exact experience when building an internal platform tool to help our solutions team ship faster. It scaffolds projects, spins up dev environments, generates configs, and deploys to staging.\n\nAs our CLI grew in scope, so did the number of our internal users. But then, something interesting happened: suddenly, we were no longer building just for human developers, but for coding agents too.\n\n## What Good DX Looks Like\n\nBefore we talk about agents, let's talk about what makes a CLI actually pleasant for human developers.\n\nSpaces is opinionated about the boring stuff. It picks sane directory structures so you don't have to. It generates the config files you'd otherwise copy from the last project. It wires services together so your API and your frontend can talk on day one, not after an hour of YAML editing or “vibing”.\n\nHere's what a typical session looks like:\n\n```text\n$ spaces init my-project$ cd my-project$ spaces dev\n```\n\nThree commands. You go from nothing to a running multi-service project with hot reload, a database, and generated Dockerfiles. That's the bar.\n\nThe commands that matter tend to fall into three buckets:\n\n- **Scaffolding** -- These create structure, ask questions, show options, let you explore.\n\n- **Development** -- These are your inner loop. They just “run”.\n\n- **Operations** -- These touch production. Careful, double-check before acting\n\nThis is all table stakes. But then our second user showed up and we discovered we needed something a little different, and a lot more interesting.\n\n## The Second User: The Agent\n\nWe built a TUI module picker for `init`. Fancy changes, commands, it looked super.\n\nThen an agent tried to use it.\n\nThe agent saw raw ANSI escape codes. `\\\\\\\\x1b[36m?\\\\\\\\x1b[0m Select components`. It couldn't send arrow keys or toggle selections. It was locked out of the command entirely.\n\nThe fix seems obvious in retrospect: add a `--components` flag. But the real insight was bigger than one flag.\n\n## Every prompt is a flag in disguise\n\nEvery time your CLI asks an interactive question, there's an implicit contract: \"I need this piece of information to proceed.\" To fulfill that contract, you can use an interactive prompt. Or you can use a flag or config file.\n\nThe trick is to think about the *information* first and the *input method* second.\n\n```python\ndef init_command(    components: str | None = Option(None),    yes: bool = Option(False, \"-y\"),):    if components:        selected = components.split(\",\")    elif yes:        selected = get_defaults()    else:        selected = show_picker()\n    # Same logic from here    create_project(selected)\n```\n\nThree input paths, one execution path. The business logic doesn't know how the inputs arrived. This means you test it once, not three times.\n\nThe `-y` flag deserves special attention. It's not just \"skip confirmations.\" It's a contract that says: *I'm providing everything you need programmatically, don't block on stdin.* When you run with `-y`, every prompt resolves to either a flag value or a smart default. If it can't, it fails loudly instead of hanging.\n\n**Example in practice: the interactive elements in this post.**\n\nThe elements were built in a fresh repository. An agent was retroactively asked to wire the project into \\[spaces cli\\] for deployment. It started by running `--help` across the relevant commands to understand the interface. From that, it figured out which configuration files were needed, generated a `config.yaml`, and connected the Dockerfile and registry settings - no hand-holding required.\n\nIn the same pass, it set up the GitHub Actions CI pipeline. From a single prompt to a live deployment took under 10 minutes (a cycle time we're actively working to bring down). After that, the repository was fully configured and the embeds were [deployed](https://spaces-blogs-staging-mistral-01d83c8e.koyeb.app/) with [Koyeb](https://www.koyeb.com/) — as a Space, scaffolded and deployed through Spaces itself. Yes, the interactive demos embedded in this blog post about Spaces are running as a Space. *Spaception*.\n\nBecause every interactive input had a flag equivalent, the agent could operate autonomously end-to-end.\n\n## Structured Data as Interface\n\nThe CLI our team was building helped our applied AI engineers ship apps. But not every app looks the same. Some need a backend and a vector database. Others just need a relational database, a frontend, and some APIs. A few are worker-only services with no UI at all.\n\nWe were not about to hardcode the module types. So we built a plugin system where each component is a plugin that declares its own properties:\n\n```python\nclass ModulePlugin(BaseModel):    type_id: str    category: str    default_port: int\n    def get_env_vars(self) -> list[EnvVarDef]: ...    def get_dev_command(self, port: int) -> str: ...\n```\n\nPlugins are introspectable. You can list them, serialize them, diff them. A human browses a TUI picker. An agent queries the registry and gets JSON back. Same data, different rendering.\n\nThis accidentally solved a problem we didn't know we had. Before, adding a new module type meant updating the picker, the Dockerfile generator, the env file writer, and the compose template. Now it means writing one plugin class. The registry is the single source of truth and everything reads from it.\n\n## Teaching Agents About Your Project\n\n> > There's an old saying that content is king. With agents, context is.\n\nThe most impactful thing we did for agent usability was generating two files on every `init`:\n\n`context.json` -- a structured snapshot of the project: what modules exist, ports they use, commands to run, env vars they need.\n\n`AGENTS.md` -- a set of rules written for LLMs and more imperative than your regular one. Not \"this project uses PostgreSQL\" but \"run `mycli dev --migrate` before testing database changes.\"\n\nAn agent that reads these files before acting makes dramatically fewer mistakes. Basically, it won’t guess at port numbers, run the wrong test command, try to install dependencies that are already managed by the toolchain.\n\nThe context file also acts as a cache-buster for stale agent assumptions. When you add a module or change a deploy target, the context file updates automatically on the next `dev` or `init`. The agent reads fresh state every time.\n\n## Implicit State is the Enemy\n\nThe subtlest problem we hit was implicit state. Our `add` command read `config.yaml` from the current working directory. A human `cd`s to the right folder without thinking. An agent running commands from a workspace root has no idea it needs to be in a subdirectory.\n\nThe fix:\n\n```python\n# Before: CWD dependencyconfig = load_config(Path.cwd() / \"config.yaml\")\n# After: explicit with fallbackconfig = load_config(    path or find_config_in_parents(Path.cwd()))\n```\n\nEvery hidden assumption -- CWD, environment variables, dotfiles in `$HOME` -- is a place where an agent will trip. Explicit parameters with sensible fallbacks solve it for agents and make scripting easier for humans too.\n\n## The Checklist\n\nLooking back, the changes were small individually. Just a set of principles applied consistently:\n\n- Every interactive input has a flag equivalent\n\n- Every flag has a smart default for headless mode\n\n- State is explicit. CWD, env vars, and config paths are inputs, not assumptions\n\n- Plugins are data models, not just code. Introspectable by default\n\n- Context files give agents (and CI, and scripts) a structured description of the project\n\n## Building a Better Tool for Everyone\n\nThe funny part is that none of this made the CLI worse for humans. The TUI picker still works and looks fancy, progress spinners still spin, confirmation dialogs still confirm. We just added a second door.\n\nAnd that second door turned out to be the more important one. Not because agents matter more than humans, but because the constraints they impose are the same constraints that make a CLI composable, scriptable, and testable. Designing for agents forced us to build a better tool for everyone.\n\nIf you're building developer tools right now, you don't need a separate agent API. You need to look at every `input()` call, every CWD assumption, every pretty-printed-only output, and ask: what if the user on the other end is a process, not a person?\n\nThe answer to that question will improve your tool either way.\n\nSpaces CLI is built by Lorenzo Signoretti, Riwa Hoteit & Sam Fenwick at Mistral AI. Special thanks to our Applied AI team, the earliest and most demanding users of the CLI, whose real-world usage shaped every pattern described here. We're excited to see the applications it will help them build with our Customers to solve painful use cases.\n\n**The tooling layer between humans and agents is still being figured out. If building developer tools at the intersection of AI and infrastructure sounds like your thing,** [**we’re hiring**](https://mistral.ai/careers)**.**\n",
  "dateCreated": "2026-10-19T01:26:53.998049939+00:00",
  "date_created": "2026-03-31T12:00:00+00:00",
  "date_modified": "2026-03-31T12:00:00+00:00",
  "date_published": "2026-03-31T12:00:00+00:00",
  "description": "Most developer tools start the same way. You need to do a thing repeatedly, so you write a script. The script grows flags. The flags grow subcommands. Before you know it, you've got a CLI. We had this exact experience when building an…",
  "external_url": "https://mistral.ai/news/spaces/",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/mistral-ai/2026/03/2026-03-31-spaces-a-cli-built-for-humans-and-agents.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/mistral-ai/2026/03/2026-03-31-spaces-a-cli-built-for-humans-and-agents.md"
  },
  "id": "urn:aggr:item:8930f8048a4ec3ce0dca73332cba17c0438480be",
  "isBasedOn": {
    "@id": "https://mistral.ai/news/spaces/"
  },
  "mainEntity": {
    "@id": "https://mistral.ai/news/spaces/",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/mistral-ai/2026-03-31-spaces-a-cli-built-for-humans-and-agents/#webpage"
    },
    "dateModified": "2026-03-31T12:00:00+00:00",
    "datePublished": "2026-03-31T12:00:00+00:00",
    "headline": "Spaces: A CLI Built for Humans and Agents",
    "image": {
      "@type": "ImageObject",
      "caption": null,
      "height": 90,
      "url": "https://mistral.ai/cms-media/api/media/file/thumbnail-03.jpg",
      "width": 160
    },
    "timeRequired": "PT7M",
    "url": "https://mistral.ai/news/spaces/",
    "wordCount": 1418
  },
  "name": "Archived snapshot: Spaces: A CLI Built for Humans and Agents",
  "preview": {
    "alt": null,
    "color": null,
    "height": 90,
    "placeholder": {
      "data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
      "hash": ""
    },
    "url": "https://mistral.ai/cms-media/api/media/file/thumbnail-03.jpg",
    "width": 160
  },
  "reading_minutes": 7,
  "replicated_at": null,
  "source": "mistral-ai",
  "source_name": "Mistral Blog",
  "summary": null,
  "tags": [],
  "temporalCoverage": "2026-10-19T01:26:53.998049939+00:00",
  "title": "Spaces: A CLI Built for Humans and Agents",
  "url": "https://aggr.aymericbeaumet.com/items/mistral-ai/2026-03-31-spaces-a-cli-built-for-humans-and-agents/",
  "word_count": 1418
}