{"slug": "show-hn-spur-solver-z3-backed-model-finder-solved-values-for-coding-agent", "title": "Show HN: Spur solver – Z3-backed model-finder solved values for coding agent", "summary": "Spur solver, a Z3-backed model-finder for SPUR coding agents, enables agents to start from solved values instead of invented parameters by returning sat with a concrete model or unsat/unknown/timeout. The tool addresses the gap where multi-variable inequalities, discrete modes, and exclusive flags form constraint systems that text-sampling LLMs cannot solve, preventing silent rule violations and brain-worker drift.", "body_md": "Constraint **model-finding** for SPUR coding agents.\n\n`spur-solver`\n\ngives brain and worker agents a Z3-backed model-finder so coding work starts from **solved values** instead of invented parameters. Agents propose constraints; the solver returns `sat`\n\nplus a concrete model (or `unsat`\n\n/ `unknown`\n\n/ `timeout`\n\n). Agents then bake those values into config, layout, infra, tests, and constants.\n\n| Without a solver | With `spur-solver` |\n|---|---|\n| Agents invent buffer sizes, grid px, replica counts | One checkable feasible assignment |\n| Silent rule violations ship | surfaces impossibility before code`unsat` |\n| Brain→worker drift on “reasonable” numbers | Shared `solve_id` / model map |\n\n**v1 scope:** model-finding only. Proofs, unsat cores, and optimization (νZ) are out of scope.\n\nDesign: `docs/superpowers/specs/2026-07-25-z3-constraint-solver-design.md`\n\nModern coding agents are excellent at *proposing* structure and terrible at *guaranteeing* multi-variable feasibility. The product thesis is a four-beat loop:\n\n```\nflowchart LR\n  B1[\"Beat 1<br/>LLM alone<br/>invents numbers\"]\n  B2[\"Beat 2<br/>Formal alone<br/>is inaccessible\"]\n  B3[\"Beat 3<br/>Merge<br/>translator + reasoner\"]\n  B4[\"Beat 4<br/>Feedback<br/>self-correct on sat/unsat\"]\n\n  B1 -->|\"guesses fail quietly\"| B2\n  B2 -->|\"agents won't write SMT by hand\"| B3\n  B3 -->|\"model or impossibility\"| B4\n  B4 -->|\"re-encode / re-query\"| B3\n\n  classDef problem fill:#3d2a2a,stroke:#e07a7a,color:#f5e6e6\n  classDef merge fill:#2a3d32,stroke:#6bcf8e,color:#e6f5ec\n  classDef feedback fill:#2a3340,stroke:#7ab0e0,color:#e6eef5\n  class B1,B2 problem\n  class B3 merge\n  class B4 feedback\n```\n\nWithout a model-finder, agents fill in numbers by feel. The drafts look confident; nothing returns `sat`\n\nor `unsat`\n\n.\n\n**Typical failure modes for coding agents:**\n\n| Scenario | Invented claim | What actually breaks |\n|---|---|---|\n| Worker pool under a memory budget | “4 workers × 128 MiB batch is fine” | OOM under real `workers * (base + k·batch)` |\n| “Safe” clamp / invariant | “Always clamp width to 320” | Violates min-main or max-viewport elsewhere |\n| UI grid layout | “Sidebar + rail + gutters fit 1440” | Overflow / negative main column |\n| Feature + HA flags | “Need 3 replicas, cap at 2” | Conflicting policy ships |\n| Brain → worker handoff | Each side picks “reasonable” values | Silent drift; tests assert different constants |\n\n```\nflowchart TB\n  subgraph Intent[\"User / task intent\"]\n    U[\"Fit indexer pool in 512 MiB<br/>≥4 workers · batch 8–128\"]\n  end\n\n  subgraph LLM[\"LLM coding agent — no solver\"]\n    direction TB\n    D1[\"Draft A: workers=8, batch=64\"]\n    D2[\"Draft B: workers=4, batch=128\"]\n    D3[\"Draft C: workers=6, batch=40\"]\n    Vibe[\"Scoring = vibes / precedent<br/>no feasibility oracle\"]\n    D1 --> Vibe\n    D2 --> Vibe\n    D3 --> Vibe\n  end\n\n  subgraph Ship[\"Shipped artifacts\"]\n    CFG[\"config.toml / constants\"]\n    TEST[\"unit tests that encode the guess\"]\n    DOC[\"plan CONTEXT with 'use N'\"]\n  end\n\n  U --> LLM\n  Vibe -->|\"picks one draft\"| CFG\n  Vibe --> TEST\n  Vibe --> DOC\n\n  FAIL[\"Silent rule violations<br/>OOM · overflow · HA conflict<br/>brain↔worker drift\"]\n  CFG --> FAIL\n  TEST --> FAIL\n  DOC --> FAIL\n\n  classDef bad fill:#4a2020,stroke:#e07a7a,color:#fdecec\n  class FAIL,Vibe bad\n```\n\n**The gap is not fluency.** The gap is that multi-variable inequalities, discrete modes, and exclusive flags form a **constraint system**. Sampling text is not solving that system.\n\nZ3 (and other SMT solvers) *can* decide these systems rigorously. Coding agents do not natively speak SMT-LIB2, manage process budgets, or hand models across brain and worker sessions.\n\n```\nflowchart LR\n  subgraph AgentWorld[\"Agent world\"]\n    NL[\"Natural language<br/>& coding intent\"]\n    Code[\"Source, configs,<br/>tests, plans\"]\n  end\n\n  subgraph FormalWorld[\"Formal world\"]\n    SMT[\"SMT-LIB2 / theories\"]\n    Z3[\"Z3 process<br/>check-sat · get-model\"]\n    Model[\"Concrete model<br/>or unsat\"]\n    SMT --> Z3 --> Model\n  end\n\n  NL -.->|\"no native bridge\"| SMT\n  Model -.->|\"no product handoff\"| Code\n\n  Note[\"Gap: agents invent numbers<br/>because formal tools are<br/>one CLI, zero integration\"]\n\n  classDef gap fill:#3d3520,stroke:#e0c07a,color:#f5f0e6\n  class Note gap\n```\n\nLeft alone, Z3 is:\n\n**Rigorous**—`sat`\n\n/`unsat`\n\n/`unknown`\n\nare honest statuses**Hostile to agent loops**— raw scripts, argv, timeouts, parsing, kill-on-cancel** Invisible to SPUR orchestration**— no`solve_id`\n\n, no MCP surface for brain and workers\n\nSo the state of the art is not “replace the LLM with Z3.” It is **give the LLM a solver tool** and let each side do what it is good at.\n\n**Product thesis:** the LLM translates intent into a closed constraint language; Z3 (subprocess) runs the strict reasoning step; agents consume the model.\n\n```\nsequenceDiagram\n  autonumber\n  participant User as User / plan task\n  participant Agent as Brain or worker LLM\n  participant MCP as SolverMcpModule\n  participant Svc as SolverService\n  participant Enc as B′ encoder / SMT gate\n  participant Z3 as Z3 subprocess\n\n  User->>Agent: Intent (budgets, ranges, modes, flags)\n  Agent->>Agent: Choose variables + constraints<br/>(do not invent final numbers)\n  Agent->>MCP: solve_constraints / solve_smt\n  MCP->>Svc: validate + schedule\n  Svc->>Enc: B′ → SMT-LIB2<br/>or raw allowlisted script\n  Enc-->>Svc: script + surface→SMT map\n  Svc->>Z3: stdin (fixed argv, wall timeout)\n  Z3-->>Svc: sat / unsat / unknown + model text\n  Svc->>Svc: decode model to surface names<br/>optional persist solve_id\n  Svc-->>MCP: status envelope\n  MCP-->>Agent: model map or unsat signal\n  Agent->>Agent: Write config / layout / tests<br/>from solved values\n```\n\n**Division of labor**\n\n```\nflowchart TB\n  subgraph Translator[\"LLM — translator\"]\n    T1[\"Extract variables & domains<br/>bool · int · int_range · enum\"]\n    T2[\"Encode rules as ConstraintExpr<br/>closed ops only\"]\n    T3[\"Interpret sat model into code\"]\n    T4[\"On unsat: relax / replan / re-encode\"]\n  end\n\n  subgraph Reasoner[\"Z3 — reasoner\"]\n    R1[\"Feasibility: check-sat\"]\n    R2[\"Witness: get-value / get-model\"]\n    R3[\"Honest incompleteness:<br/>unknown · timeout\"]\n  end\n\n  T1 --> T2 --> R1\n  R1 -->|\"sat\"| R2 --> T3\n  R1 -->|\"unsat\"| T4\n  R1 -->|\"unknown / timeout\"| T4\n\n  classDef llm fill:#2a3340,stroke:#7ab0e0,color:#e6eef5\n  classDef z3 fill:#2a3d32,stroke:#6bcf8e,color:#e6f5ec\n  class T1,T2,T3,T4 llm\n  class R1,R2,R3 z3\n```\n\nWorked sketch — **indexer pool under 512 MiB**:\n\n```\nvars:\n  workers ∈ [1, 16]\n  batch   ∈ [8, 128]\nconstraints:\n  workers ≥ 4\n  workers * (48 + 2 * batch) ≤ 512\n```\n\nOne feasible model (Z3 may return any sat assignment): `{ \"workers\": 4, \"batch\": 40 }`\n\n. The agent ships *that* assignment and tests the inequality — not a vibes draft.\n\nA single solve is not the whole story. The durable advantage is the **feedback loop**: statuses flow back so the agent can re-encode instead of double-down on a guess.\n\n``` php\nstateDiagram-v2\n  [*] --> Encode: intent + bounds\n  Encode --> Solve: solve_constraints / solve_smt\n\n  Solve --> Sat: status = sat\n  Solve --> Unsat: status = unsat\n  Solve --> Incomplete: status = unknown | timeout\n  Solve --> Error: validation / spawn / parse\n\n  Sat --> Consume: bake model into code & CONTEXT\n  Consume --> Persist: optional persist=true → solve_id\n  Persist --> Handoff: worker get_solve_result\n  Handoff --> [*]\n\n  Unsat --> Diagnose: rules over-constrained\n  Diagnose --> Reencode: drop / relax / split task\n  Reencode --> Encode\n\n  Incomplete --> Budget: tighten query or raise timeout ≤ 60s\n  Budget --> Encode\n  Incomplete --> Escalate: do not treat as unsat\n\n  Error --> FixRequest: fix schema / install Z3 / shrink script\n  FixRequest --> Encode\n```\n\n**Status discipline (normative):**\n\n| Status | Meaning | Agent should |\n|---|---|---|\n`sat` |\nFeasible model present | Consume `model` ; optionally persist |\n`unsat` |\nNo assignment exists | Replan / relax constraints — do not invent values |\n`unknown` |\nSolver incomplete | Retry or simplify — never collapse to `unsat` |\n`timeout` |\nWall clock exceeded | Retry with smaller problem or higher budget ≤ 60s |\n`error` / MCP error |\nInvalid params, missing Z3, parse failure | Fix request or operator install |\n\nBrain → worker handoff:\n\n```\nsequenceDiagram\n  participant Brain as Brain agent\n  participant Svc as SolverService\n  participant Disk as .spur/solver/\n  participant Worker as Worker agent\n\n  Brain->>Svc: solve_constraints(persist=true)\n  Svc->>Disk: atomic write sol_&lt;16hex&gt;.json\n  Svc-->>Brain: sat + model + solve_id\n  Brain->>Worker: task CONTEXT embeds solve_id + key fields\n  Worker->>Svc: get_solve_result(solve_id)\n  Svc->>Disk: load artifact\n  Svc-->>Worker: authoritative result envelope\n  Note over Worker: Workers use the tool,<br/>not ad-hoc file IO\n```\n\n| Surface | Role |\n|---|---|\n`solve_constraints` |\nPreferred path: typed B′ variables + `ConstraintExpr` AST → model-finding |\n`solve_smt` |\nEscape hatch: raw SMT-LIB2 with size + command allowlist |\n`get_solve_result` |\nReload a persisted `solve_id` for brain→worker handoff |\n`SolverService` |\nProcess-wide owner: discovery, semaphore, spawn/kill, decode, persist |\n`SolverMcpModule` |\nThin MCP `ToolModule` adapter (live or `catalog_only` ) |\n\n```\nflowchart TB\n  subgraph Callers[\"Callers\"]\n    Brain[\"Brain MCP registry\"]\n    Worker[\"Worker MCP registry\"]\n  end\n\n  subgraph Crate[\"crates/spur-solver\"]\n    MCP[\"mcp::SolverMcpModule\"]\n    Svc[\"service::SolverService\"]\n    Types[\"types — B′ request/response\"]\n    Encode[\"encode — B′ → SMT-LIB2\"]\n    Gate[\"smt_gate — raw allowlist\"]\n    Proc[\"process — Z3Process runner\"]\n    Persist[\"persist — .spur/solver artifacts\"]\n  end\n\n  Z3bin[\"External z3 binary<br/>SPUR_Z3_BIN → PATH\"]\n\n  Brain --> MCP\n  Worker --> MCP\n  MCP --> Svc\n  Svc --> Types\n  Svc --> Encode\n  Svc --> Gate\n  Svc --> Proc\n  Svc --> Persist\n  Proc --> Z3bin\n```\n\n**Why subprocess (not FFI):**\n\n- Avoid\n`!Send`\n\n/`!Sync`\n\nZ3 contexts in async MCP handlers - Crash isolation from the main\n`spur`\n\nprocess - Packaging already heavy (DuckDB); another linked C++ lib hurts zigbuild / xwin / CI\n- Runner is mockable with a fake script (see tests)\n\n```\nsrc/\n├── lib.rs        # Crate root\n├── types.rs      # Variables, ConstraintExpr, requests, statuses, validation\n├── encode.rs     # B′ → SMT-LIB2 + identifier mangling\n├── smt_gate.rs   # Raw SMT-LIB2 size + command allowlist\n├── process.rs    # ProcessRunner trait + Z3Process (kill-on-drop / group)\n├── service.rs    # SolverService: semaphore, decode, orchestrate\n├── persist.rs    # Atomic .spur/solver/<solve_id>.json store\n└── mcp.rs        # SolverMcpModule + tool schemas\n```\n\nVariable `type` |\nFields | SMT sort |\n|---|---|---|\n`bool` |\n`name` |\nBool |\n`int` |\n`name` |\nInt (prefer `int_range` when bounds known) |\n`int_range` |\n`name` , `min` , `max` |\nInt + bound asserts |\n`enum` |\n`name` , `values[]` |\nInt indices; model maps back to labels |\n\n| Op class | Ops | Notes |\n|---|---|---|\n| Compare | `eq` , `ne` , `lt` , `le` , `gt` , `ge` |\nEnums: only `eq` / `ne` vs labels |\n| Arith | `add` , `sub` , `mul` |\nNo `div` in v1 |\n| Bool | `and` , `or` , `not` |\nTop-level constraints must be Bool |\n\nEvery expression node is tagged (`kind`\n\n: `var`\n\n| `int`\n\n| `bool`\n\n| `enum_label`\n\n| `op`\n\n). Bare strings/numbers are rejected.\n\n| Parameter | Default |\n|---|---|\n`timeout_ms` |\n30_000 (hard cap 60_000) |\n| Max concurrent Z3 children | 4 (process-wide semaphore) |\n| Max variables / constraints | 64 / 256 |\n| Max expression depth | 32 |\n| Z3 memory soft cap | 1024 MiB |\n| Persist root | `<repo_root>/.spur/solver/` |\n`solve_id` |\n`^sol_[0-9a-f]{16}$` |\n\nZ3 is **operator-installed**, not agent-supplied:\n\n- Install Z3 so\n`z3`\n\nis on`PATH`\n\n,**or** set`SPUR_Z3_BIN`\n\nto the binary. - Agents never pass executable paths or custom Z3 argv.\n- Missing binary → structured\n`solver_unavailable`\n\n(no silent download in v1).\n\n```\n# Unit / fake-runner tests (no Z3 required)\nscripts/spur-cargo test -p spur-solver\n\n# Real Z3 path (binary required)\nSPUR_TEST_Z3=1 scripts/spur-cargo test -p spur-solver --test real_z3\n{\n  \"vars\": [\n    { \"type\": \"int_range\", \"name\": \"workers\", \"min\": 1, \"max\": 16 },\n    { \"type\": \"int_range\", \"name\": \"batch\", \"min\": 8, \"max\": 128 }\n  ],\n  \"constraints\": [\n    {\n      \"kind\": \"op\",\n      \"op\": \"ge\",\n      \"args\": [\n        { \"kind\": \"var\", \"name\": \"workers\" },\n        { \"kind\": \"int\", \"value\": 4 }\n      ]\n    },\n    {\n      \"kind\": \"op\",\n      \"op\": \"le\",\n      \"args\": [\n        {\n          \"kind\": \"op\",\n          \"op\": \"mul\",\n          \"args\": [\n            { \"kind\": \"var\", \"name\": \"workers\" },\n            {\n              \"kind\": \"op\",\n              \"op\": \"add\",\n              \"args\": [\n                { \"kind\": \"int\", \"value\": 48 },\n                {\n                  \"kind\": \"op\",\n                  \"op\": \"mul\",\n                  \"args\": [\n                    { \"kind\": \"int\", \"value\": 2 },\n                    { \"kind\": \"var\", \"name\": \"batch\" }\n                  ]\n                }\n              ]\n            }\n          ]\n        },\n        { \"kind\": \"int\", \"value\": 512 }\n      ]\n    }\n  ],\n  \"timeout_ms\": 30000,\n  \"persist\": true\n}\n```\n\nIllustrative success envelope:\n\n```\n{\n  \"status\": \"sat\",\n  \"model\": { \"workers\": 4, \"batch\": 40 },\n  \"duration_ms\": 12,\n  \"solve_id\": \"sol_a1b2c3d4e5f67890\",\n  \"reason\": null\n}\n```\n\n- Full program verification / typechecking replacement\n- Proof generation, unsat cores, interpolants\n- First-class νZ maximize/minimize (re-query with tighter bounds instead)\n- JSON theories: Real, BitVec, String, Array, Float, quantifiers (use\n`solve_smt`\n\nif needed) - Bundling Z3 into dist artifacts / auto-download\n- TUI surface for solves\n\n| Doc / artifact | Role |\n|---|---|\n|", "url": "https://wpnews.pro/news/show-hn-spur-solver-z3-backed-model-finder-solved-values-for-coding-agent", "canonical_source": "https://github.com/getspur/spur/tree/main/crates/spur-solver", "published_at": "2026-07-27 04:47:37+00:00", "updated_at": "2026-07-27 05:22:37.026092+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-tools"], "entities": ["Spur solver", "Z3", "SPUR"], "alternates": {"html": "https://wpnews.pro/news/show-hn-spur-solver-z3-backed-model-finder-solved-values-for-coding-agent", "markdown": "https://wpnews.pro/news/show-hn-spur-solver-z3-backed-model-finder-solved-values-for-coding-agent.md", "text": "https://wpnews.pro/news/show-hn-spur-solver-z3-backed-model-finder-solved-values-for-coding-agent.txt", "jsonld": "https://wpnews.pro/news/show-hn-spur-solver-z3-backed-model-finder-solved-values-for-coding-agent.jsonld"}}