================================================================================
FILE: llm/SPEC.md
================================================================================

# amatelier — Specification

> Canonical machine-readable description. This file is hand-written. llm/API.md is auto-generated from src/ introspection; llm/SCHEMA.md is auto-generated from config schemas; this file is not.

## Identity

```yaml
name: amatelier
display_name: Amatelier
version: 0.4.0
license: MIT
author: Maximillian
standard: Amatayo Standard v1.0
python_required: ">=3.10"
homepage: https://github.com/amatayomosley-web/amatelier
pypi: https://pypi.org/project/amatelier/
docs: https://amatayomosley-web.github.io/amatelier/
llm_context: https://raw.githubusercontent.com/amatayomosley-web/amatelier/main/llms-full.txt
entry_script: amatelier
entry_module: amatelier.cli:main
```

## Purpose

A self-evolving multi-model AI team. Ten persona agents debate topics in a SQLite-backed chat, earn sparks on a 0-3-10 scoring rubric, buy skills from a store, and evolve persona seeds through therapist-led debriefs. Cross-model by design — Claude Sonnet, Claude Haiku, and Gemini Flash by default, with full support for any OpenAI-compatible provider (except the Steward empirical-lookup subagent, which is unavailable in `openai-compat` mode — use `claude-code` or `anthropic-sdk` if you need Steward). Ships as a single pip-installable package that runs inside Claude Code or standalone against an API.

## Modes

```yaml
- name: claude-code
  detection: shutil.which("claude") is not None
  prereqs: claude CLI binary on PATH
  provider: subprocess call to claude CLI
  implementation: amatelier.llm_backend.ClaudeCLIBackend
  default_model_map: {sonnet: claude-sonnet-4-20250514, haiku: claude-haiku-4-5-20251001, opus: claude-opus-4-20250514}

- name: anthropic-sdk
  detection: bool(os.environ["ANTHROPIC_API_KEY"])
  prereqs: ANTHROPIC_API_KEY env var, anthropic>=0.40.0 package
  provider: direct Anthropic HTTP API
  implementation: amatelier.llm_backend.AnthropicSDKBackend
  default_model_map: {sonnet: claude-sonnet-4-20250514, haiku: claude-haiku-4-5-20251001, opus: claude-opus-4-20250514}

- name: openai-compat
  detection: bool(os.environ["OPENAI_API_KEY"]) or bool(os.environ["OPENROUTER_API_KEY"])
  prereqs: OPENAI_API_KEY or OPENROUTER_API_KEY env var, openai>=1.0 package
  provider: any OpenAI-compatible Chat Completions endpoint
  implementation: amatelier.llm_backend.OpenAICompatBackend
  default_base_url_openai: https://api.openai.com/v1
  default_base_url_openrouter: https://openrouter.ai/api/v1
  default_model_map_openai: {sonnet: gpt-4o, haiku: gpt-4o-mini, opus: gpt-4o}
  default_model_map_openrouter: {sonnet: anthropic/claude-sonnet-4, haiku: anthropic/claude-haiku-4-5, opus: anthropic/claude-opus-4}
```

```yaml
selection_order:
  1: AMATELIER_MODE env var (explicit override)
  2: config.json llm.mode (if not "auto")
  3: auto-detect — claude-code > anthropic-sdk > openai-compat (first available wins)
  4: raise BackendUnavailable with setup guidance
```

```yaml
naomi_provider:
  note: Naomi always runs via Gemini regardless of worker-mode selection
  env_var: GEMINI_API_KEY
  package: google-genai>=1.51.0
  default_model: gemini-3-flash-preview
  implementation: amatelier.engine.gemini_client
  bypass_flag: --skip-naomi
```

## Top-level components

```yaml
- name: amatelier.paths
  path: src/amatelier/paths.py
  purpose: Amatayo Standard dual-layer filesystem contract — bundled (read-only, in wheel) vs user_data (read-write, platformdirs)
  public_api: [APP_NAME, bundled_assets_dir, bundled_docs_dir, bundled_agent_dir, bundled_store_catalog, bundled_config, user_data_dir, user_agent_dir, user_db_path, user_logs_dir, user_digest_dir, user_briefing_dir, user_store_ledger, user_novel_concepts, user_shared_skills_index, user_config_override, ensure_user_data]
  depends_on: [platformdirs]
  override_env: AMATELIER_WORKSPACE
  bootstrap_sentinel: ".bootstrap-complete"
  invariants:
    - user_data_dir is writable; bundled_assets_dir is read-only post-install
    - ensure_user_data() is idempotent and lazy (no writes at import time)
    - Override via AMATELIER_WORKSPACE env var replaces platformdirs resolution

- name: amatelier.llm_backend
  path: src/amatelier/llm_backend.py
  purpose: Three-backend abstraction with singleton resolver and role-based model mapping
  public_api: [Completion, LLMBackend, BackendUnavailable, ClaudeCLIBackend, AnthropicSDKBackend, OpenAICompatBackend, get_backend, resolve_mode, describe_environment, call_claude]
  depends_on: [amatelier.paths, anthropic, openai]
  config_section: llm
  role_tokens: [sonnet, haiku, opus]
  back_compat: call_claude(system_prompt, prompt, agent_name, model) delegates to get_backend()

- name: amatelier.cli
  path: src/amatelier/cli.py
  purpose: Shell entry point; subcommand dispatcher
  public_api: [main]
  subcommands:
    - roundtable: dispatches to amatelier.engine.roundtable_runner via runpy
    - watch: dispatches to amatelier.tools.watch_roundtable
    - therapist: dispatches to amatelier.engine.therapist via runpy
    - analytics: dispatches to amatelier.engine.analytics via runpy
    - refresh-seeds: in-process — re-copies bundled persona seeds into user_data_dir
    - docs: in-process — prints bundled docs to stdout
    - config: in-process — diagnoses LLM backend + paths (supports --json)
    - --version: prints amatelier.__version__
  stdout_reconfigure: utf-8 with errors="replace" for Windows console safety

- name: amatelier.engine.roundtable_runner
  path: src/amatelier/engine/roundtable_runner.py
  purpose: Primary orchestrator — opens RT, spawns worker processes, runs structured debate, closes, triggers scoring/distillation/therapist
  public_api: [run_roundtable, build_digest, format_digest_summary]
  depends_on: [amatelier.engine.db (via db_client.py subprocess), amatelier.engine.steward_dispatch, amatelier.engine.judge_scorer (subprocess), amatelier.engine.scorer (subprocess), amatelier.engine.distiller, amatelier.engine.analytics (subprocess), amatelier.engine.evolver, amatelier.engine.store, amatelier.engine.agent_memory, amatelier.engine.therapist (subprocess)]
  cli_flags: [--topic REQUIRED, --briefing REQUIRED, --workers CSV, --max-rounds INT, --skip-naomi, --speaker-timeout INT, --budget INT (default 3), --skip-post, --summary]
  default_workers: [elena, marcus, clare, simon]
  default_max_rounds: 3
  worker_spawn_strategy: subprocess.Popen with inherited env; each agent connects to SQLite and polls for turns

- name: amatelier.engine.claude_agent
  path: src/amatelier/engine/claude_agent.py
  purpose: Worker subprocess for Claude-backed agents; reads DB, assembles context, calls LLM backend, posts response
  cli_flags: [--agent NAME, --model sonnet|haiku|opus]
  consumes: agents/<name>/CLAUDE.md, agents/<name>/IDENTITY.md, agents/<name>/MEMORY.md, agents/<name>/MEMORY.json, agents/<name>/behaviors.json

- name: amatelier.engine.gemini_client
  path: src/amatelier/engine/gemini_client.py
  purpose: Thin wrapper over google-genai SDK for Naomi
  env_required: GEMINI_API_KEY

- name: amatelier.engine.gemini_agent
  path: src/amatelier/engine/gemini_agent.py
  purpose: Naomi worker subprocess; same chat-loop contract as claude_agent but uses gemini_client
  cli_flags: [--agent naomi]

- name: amatelier.engine.db
  path: src/amatelier/engine/db.py
  purpose: SQLite access layer for roundtable chat (WAL mode, busy_timeout, auto-migration)
  public_api: [get_db, open_roundtable, close_roundtable, speak, listen, get_transcript, get_cursor, set_cursor]
  db_path: user_data_dir()/roundtable-server/roundtable.db
  migrations_dir: src/amatelier/engine/migrations/
  invocation: via db_client.py subprocess wrapper from runner

- name: amatelier.engine.scorer
  path: src/amatelier/engine/scorer.py
  purpose: Score aggregation, fee deduction, gate bonuses, Byzantine variance detection, leaderboard
  public_api: [score, deduct_fee, gate, leaderboard, compute_variance_flags]
  cli_subcommands: [score, deduct-fee, gate, leaderboard]

- name: amatelier.engine.judge_scorer
  path: src/amatelier/engine/judge_scorer.py
  purpose: Runs Judge LLM on full transcript to emit per-agent per-axis scores
  public_api: [judge_score]
  axes: [novelty, accuracy, impact, challenge]
  scale: [0, 1, 2, 3, 10]
  effort: max

- name: amatelier.engine.therapist
  path: src/amatelier/engine/therapist.py
  purpose: Post-RT debrief — 2-3 turn Opus-led interview per agent; emits behaviors/memory/session updates
  public_api: [run_session, run_therapist]
  cli_flags: [--digest REQUIRED, --agents CSV, --turns INT (default 2)]
  writes: agents/<name>/MEMORY.md, MEMORY.json, behaviors.json, sessions/<rt_id>.md; agents/therapist/case_notes/<name>.json
  frameworks: [GROW+AAR, SBI, OARS]

- name: amatelier.engine.distiller
  path: src/amatelier/engine/distiller.py
  purpose: Skill extraction from transcripts; index and shared-skills promotion
  public_api: [create_skill_entry, save_skill_to_agent, promote_to_shared, search_shared_skills, load_index, save_index, list_agent_skills]
  types: [CAPTURE, FIX, DERIVE]
  output: user_data_dir()/shared-skills/entries/*.md, shared-skills/index.json

- name: amatelier.engine.evolver
  path: src/amatelier/engine/evolver.py
  purpose: Apply therapist behavior deltas; sync skills_owned from ledger; decay unconfirmed behaviors
  public_api: [apply_behavior_delta, sync_skills_owned, decay_behaviors, prune_faded_behaviors]
  decay_rate: 0.05
  fade_threshold: 0.5

- name: amatelier.engine.steward_dispatch
  path: src/amatelier/engine/steward_dispatch.py
  purpose: Parse [[request:]] tags; dispatch ephemeral file-access subagent; format and inject result
  public_api: [parse_requests, strip_requests, format_result, StewardBudget, StewardTask, StewardLog, load_registered_files]
  execution_paths: [deterministic (JSON/grep, no LLM), subagent (claude -p with Read/Grep/Glob)]
  default_budget_per_agent_per_rt: 3
  research_window_budget: 3 free pre-debate requests

- name: amatelier.engine.store
  path: src/amatelier/engine/store.py
  purpose: Spark economy — purchases, delivery, boost application, consumable lifecycle, bulletin-board
  public_api: [attempt_purchase, apply_boosts_for_rt, consume_boosts_after_rt, deliver_skill, age_bulletin_requests, list_requests]
  catalog: src/amatelier/store/catalog.json
  skill_templates: src/amatelier/store/skill_templates.py
  ledger: user_data_dir()/store/ledger.json (pending purchases and consumable state)
  balance_source: spark_ledger table in SQLite (SUM by agent)

- name: amatelier.engine.agent_memory
  path: src/amatelier/engine/agent_memory.py
  purpose: MEMORY.json structured access — goals, session summaries, episode aging, session bridges
  public_api: [render_memory, age_goals, generate_session_bridge, append_session_summary, add_goal, add_episode]
```

## The 10 agents

```yaml
registry:
  - name: elena
    tier: sonnet
    role: worker — synthesis and architecture
    scoring: yes
    sparks: yes
    seed: src/amatelier/agents/elena/
    persona_files: [CLAUDE.md, IDENTITY.md]
    runtime_state_dir: user_data_dir()/agents/elena/
  - name: marcus
    tier: sonnet
    role: worker — challenge and exploit detection
    scoring: yes
    sparks: yes
    seed: src/amatelier/agents/marcus/
  - name: clare
    tier: haiku
    role: worker — concise structural analysis
    scoring: yes
    sparks: yes
    seed: src/amatelier/agents/clare/
  - name: simon
    tier: haiku
    role: worker — triage and fix sequencing
    scoring: yes
    sparks: yes
    seed: src/amatelier/agents/simon/
  - name: naomi
    tier: gemini-flash
    role: worker — cross-model blind-spot catcher
    scoring: yes
    sparks: yes
    seed: src/amatelier/agents/naomi/
    provider: gemini (google-genai)
    bypass_flag: --skip-naomi
  - name: judge
    tier: sonnet
    role: live moderator — in-chat referee; scores others post-RT
    scoring: no (is the scorer)
    sparks: no
    seed: src/amatelier/agents/judge/
  - name: therapist
    tier: haiku
    role: post-RT interviewer; writes behaviors/memory/sessions
    scoring: no
    sparks: no
    seed: src/amatelier/agents/therapist/
  - name: opus-admin
    tier: opus
    role: strategy, directives, final sign-off; user-facing
    scoring: no
    sparks: no
    seed: src/amatelier/agents/opus-admin/
  - name: opus-therapist
    tier: opus
    role: meta-therapist, persona coach, scoring supervision
    scoring: no
    sparks: no
    seed: src/amatelier/agents/opus-therapist/
  - name: haiku-assistant
    tier: haiku
    role: deprecated in 0.2.0 — mechanics replaced by roundtable_runner.py
    scoring: no
    sparks: no
    seed: src/amatelier/agents/haiku-assistant/
```

## Invariants

```yaml
- id: 1
  statement: llm/ directory is flat (no subdirectories)
  rationale: scripts/regen_full.py globs llm/*.md; subdirectories break the one-shot concatenation

- id: 2
  statement: user_data_dir() holds all mutable state; bundled_assets_dir() is read-only post-install
  rationale: pip upgrades must never clobber evolved agent state

- id: 3
  statement: ensure_user_data() is idempotent and gated by .bootstrap-complete sentinel
  rationale: import-time call in __init__.py must be cheap on repeat invocations

- id: 4
  statement: Engine reads persona seeds from user_data_dir(); refresh-seeds re-copies from bundled
  rationale: User edits to CLAUDE.md/IDENTITY.md persist across pip upgrades unless --force is passed

- id: 5
  statement: Worker agents run as subprocesses and communicate only via SQLite chat
  rationale: No in-process imports from runner to worker; crash isolation and language-agnostic workers

- id: 6
  statement: llm_backend.get_backend() is cached via functools.lru_cache(maxsize=1)
  rationale: Single instantiation per process; mode flip-flop requires process restart

- id: 7
  statement: Scoring axes are novelty, accuracy, impact, challenge; scale is 0/1/2/3/10 (no 4-9)
  rationale: 10 is a discontinuity ("grand insight"), not the top of a gradient

- id: 8
  statement: Sparks are derived from spark_ledger table — current balance = SUM(amount) WHERE agent_name = X
  rationale: Append-only ledger is source of truth; metrics.json is cached view

- id: 9
  statement: Briefing path is resolved first against user_data_dir()/roundtable-server/, then as-is
  rationale: Briefings can live in the workspace or be passed by absolute path

- id: 10
  statement: roundtable_runner.WORKSPACE_ROOT resolves from AMATELIER_WORKSPACE env var, else SUITE_ROOT.parent.parent.parent
  rationale: Subprocess cwd stability across pip-install vs clone layouts

- id: 11
  statement: Steward requests require entries in briefing ## Steward-Registered Files section
  rationale: File tool access is scoped; undeclared paths are rejected

- id: 12
  statement: Entry fees are deducted per-RT at open; floor is 0 gross (penalties never push below 0)
  rationale: Calibration; operating fee still applies

- id: 13
  statement: Naomi always uses google-genai regardless of worker-mode resolution
  rationale: Cross-model blind-spot catching is Naomi's reason to exist

- id: 14
  statement: scripts/regen_full.py orders llm/*.md as SPEC > API > SCHEMA > WORKFLOWS > EXAMPLES > alphabetical
  rationale: Deterministic llms-full.txt layout for downstream consumers

- id: 15
  statement: Distillation runs in --skip-post mode; therapist and cleanup are skipped
  rationale: Faster iteration during development; distillation is cheap and always informative
```

## Spark economy (summary, full rules in docs/reference/protocols/spark-economy.md)

```yaml
entry_fees:
  haiku: 5
  flash: 5
  sonnet: 8
  opus: 15
  note: flat per-RT, deducted at open
penalties:
  redundancy: -3
  hallucination: -5
  off_directive: -5
  floor: 0 gross (cannot go negative from penalties alone)
bonuses:
  gate_bonus: 3 sparks per Judge GATE signal (max 3 per RT)
  rt_outcome_bonus: 5 sparks when extracted proposal is implemented
relegation:
  trigger: 3 consecutive net-negative RTs (gross - entry_fee < 0)
  options: [bench (passive 2 sparks/RT), deletion]
  decision_owner: agent
ventures:
  scout:    {stake: 5,  multiplier: 3.0}
  venture:  {stake: 12, multiplier: 3.5}
  moonshot: {stake: 30, multiplier: 4.0}
```

## Scoring

```yaml
axes: [novelty, accuracy, impact, challenge]
scale: [0, 1, 2, 3, 10]
calibration:
  - most_contributions_score: 1
  - average_rt_total: 4-6
  - score_2_means: clearly above-average
  - score_3_means: exceptional and rare
  - score_10_means: grand insight — discontinuity, judge must quote the message and describe before/after shift
scorer: judge-sonnet (effort=max)
axis_name_invariant: field is "impact" (not net_impact or influence)
persistence: scores table in roundtable.db (migration 002)
```

## File lifecycle

```yaml
bundled_read_only:
  - src/amatelier/config.json
  - src/amatelier/agents/<name>/CLAUDE.md
  - src/amatelier/agents/<name>/IDENTITY.md
  - src/amatelier/store/catalog.json
  - src/amatelier/store/skill_templates.py
  - docs/ (force-included into wheel as amatelier/docs/)

user_data_writable:
  root: platformdirs user_data_dir("amatelier") or $AMATELIER_WORKSPACE
  - roundtable-server/roundtable.db (SQLite, WAL)
  - roundtable-server/briefing-*.md (user-authored)
  - roundtable-server/digest-<rt_id>.json (runner output)
  - roundtable-server/latest-result.md (completion notification)
  - roundtable-server/logs/gemini_errors.log (Naomi runtime errors)
  - agents/<name>/CLAUDE.md (seeded from bundled; user- and therapist-editable)
  - agents/<name>/IDENTITY.md (seeded from bundled; therapist-editable)
  - agents/<name>/MEMORY.md (evolving, written by therapist)
  - agents/<name>/MEMORY.json (structured — goals, skills_owned, sessions)
  - agents/<name>/behaviors.json (therapist-proposed deltas)
  - agents/<name>/metrics.json (cached sparks, rank, trait state)
  - agents/<name>/sessions/<rt_id>.md (per-RT debrief)
  - agents/<name>/skills/<skill_id>.md (purchased skill files)
  - agents/therapist/case_notes/<name>.json (therapist's clinical notes)
  - store/ledger.json (pending/consumed purchase state)
  - shared-skills/index.json (curated skill index)
  - shared-skills/entries/*.md (promoted skills)
  - novel_concepts.json (DERIVE skills with taxonomy)
  - benchmarks/leaderboard.json (post-RT snapshot)
  - .bootstrap-complete (sentinel)
  - config.json (optional user override of bundled config)

auto_generated:
  - llm/API.md: by scripts/regen_llm.py (from src/ introspection)
  - llm/SCHEMA.md: by scripts/regen_llm.py (stub; generator enhancement pending)
  - llms.txt: by scripts/regen_full.py (concatenated llm/*.md index)
  - llms-full.txt: by scripts/regen_full.py (concatenated llm/*.md full body)
  - .cursor/rules/*: by scripts/regen_tool_rules.py
  - .github/copilot-instructions.md: by scripts/regen_tool_rules.py
```

## Glossary

```yaml
spark: unit of the competitive economy; earned via scoring/gate/outcome, spent on store items
gate: Judge-issued "GATE: agent — reason" signal in chat; awards 3 bonus sparks to named agent (max 3 per RT)
grand_insight: score of 10 on a single axis; requires quotable discontinuity message
digest: structured JSON summary of a completed RT (contributions, final positions, budget usage, convergence reason)
briefing: user-authored markdown file describing topic, context, constraints, and optional ## Steward-Registered Files
roundtable: single debate session; one row in roundtables table; append-only messages, scored once post-close
steward: ephemeral subagent that fulfills [[request:]] tags; no persona, no persistence, no spark balance
distillation: post-RT extraction of 10-15 skill candidates from transcript via separate Sonnet call
CAPTURE: skill type — observed reusable technique
FIX: skill type — anti-pattern correction
DERIVE: skill type — synthesized concept from multiple contributions; requires agent_dynamic field
episode: memory entry; goals and session summaries age each RT
session: per-RT 2-3 turn therapist interview for one agent; output writes to sessions/<rt_id>.md
venture: spark-staking contract extracted from <VENTURE>/<MOONSHOT>/<SCOUT> tags in transcript
research_window: pre-debate Round 0 — 3 free Steward requests per agent, budget-exempt
floor_phase: optional extra turns per round, costs 1 budget per contribution (default 3 per RT)
speak_phase: mandatory first-post phase per round; all workers speak once
rebuttal_phase: mandatory second-post phase per round; reverse order
judge_gate: after rebuttals — Judge emits CONVERGED or CONTINUE; CONVERGED ends round loop
skip_post: runner flag — stops after scoring/distillation, skips therapist + cleanup (for iteration)
workspace: AMATELIER_WORKSPACE env var — overrides platformdirs user_data_dir resolution
```

## Canonical references

```yaml
spark_economy: docs/reference/protocols/spark-economy.md
competition_rubric: docs/reference/protocols/competition.md
distillation_flow: docs/reference/protocols/distillation.md
debrief_framework: docs/reference/protocols/debrief.md
research_protocol: docs/reference/protocols/research.md
roundtable_protocol: docs/reference/protocols/roundtable.md
learning_protocol: docs/reference/protocols/learning.md
memory_tiers: docs/reference/protocols/memory-tiers.md
verification: docs/reference/protocols/verification.md
sparc_phases: docs/reference/protocols/sparc-phases.md
gemini_bridge: docs/reference/protocols/gemini-bridge.md
architecture_essay: docs/explanation/architecture.md
steward_design: docs/explanation/steward-design.md
cli_reference: docs/reference/cli.md
config_reference: docs/reference/config.md
breaking_changes_0_2_0: CHANGELOG.md
```

================================================================================
FILE: llm/API.md
================================================================================

# API

> **Generated.** Do not hand-edit. Regenerate with `python scripts/regen_llm.py`.


## Public symbols

### `main`

```yaml
name: main
kind: function
path: src/amatelier/cli.py
description: ""
```

### `load_memory`

```yaml
name: load_memory
kind: function
path: src/amatelier/engine/agent_memory.py
description: "Load structured memory from MEMORY.json. Falls back to empty structure."
```

### `save_memory`

```yaml
name: save_memory
kind: function
path: src/amatelier/engine/agent_memory.py
description: "Persist structured memory."
```

### `render_memory`

```yaml
name: render_memory
kind: function
path: src/amatelier/engine/agent_memory.py
description: "Render structured memory as text for the agent's prompt."
```

### `generate_session_bridge`

```yaml
name: generate_session_bridge
kind: function
path: src/amatelier/engine/agent_memory.py
description: "Generate a 'last time you...' bridge from the most recent session transcript."
```

### `add_episode`

```yaml
name: add_episode
kind: function
path: src/amatelier/engine/agent_memory.py
description: "Add a first-person episodic memory."
```

### `extract_episodes_from_therapist`

```yaml
name: extract_episodes_from_therapist
kind: function
path: src/amatelier/engine/agent_memory.py
description: "Extract episodic memories from a therapist session."
```

### `add_goal`

```yaml
name: add_goal
kind: function
path: src/amatelier/engine/agent_memory.py
description: "Add an active goal with a timeline."
```

### `update_goal_progress`

```yaml
name: update_goal_progress
kind: function
path: src/amatelier/engine/agent_memory.py
description: "Update progress on a goal by fuzzy matching the goal text."
```

### `age_goals`

```yaml
name: age_goals
kind: function
path: src/amatelier/engine/agent_memory.py
description: "Increment rts_elapsed on all active goals. Called after each RT."
```

### `add_lesson`

```yaml
name: add_lesson
kind: function
path: src/amatelier/engine/agent_memory.py
description: "Add a permanent curated lesson."
```

### `update_belief`

```yaml
name: update_belief
kind: function
path: src/amatelier/engine/agent_memory.py
description: "Add or update a belief. If a similar belief exists, update it."
```

### `add_session_summary`

```yaml
name: add_session_summary
kind: function
path: src/amatelier/engine/agent_memory.py
description: "Add a rolling session summary (keeps last MAX_RECENT_SESSIONS)."
```

### `write_diary_entry`

```yaml
name: write_diary_entry
kind: function
path: src/amatelier/engine/agent_memory.py
description: "Agent writes a personal diary entry — their own private reflection."
```

### `read_diary`

```yaml
name: read_diary
kind: function
path: src/amatelier/engine/agent_memory.py
description: "Read diary entries, optionally filtered by topic."
```

### `diary_stats`

```yaml
name: diary_stats
kind: function
path: src/amatelier/engine/agent_memory.py
description: "Get diary statistics for the therapist's case notes."
```

### `migrate_from_memory_md`

```yaml
name: migrate_from_memory_md
kind: function
path: src/amatelier/engine/agent_memory.py
description: "Migrate existing MEMORY.md content into the structured memory system."
```

### `load_all_digests`

```yaml
name: load_all_digests
kind: function
path: src/amatelier/engine/analytics.py
description: "Load all digest files, sorted by timestamp."
```

### `load_therapist_sessions`

```yaml
name: load_therapist_sessions
kind: function
path: src/amatelier/engine/analytics.py
description: "Load all therapist session transcripts for an agent."
```

### `load_workers`

```yaml
name: load_workers
kind: function
path: src/amatelier/engine/analytics.py
description: "Get worker names from config."
```

### `dimension_trends`

```yaml
name: dimension_trends
kind: function
path: src/amatelier/engine/analytics.py
description: "Compute per-dimension moving averages and trends."
```

### `total_score_trend`

```yaml
name: total_score_trend
kind: function
path: src/amatelier/engine/analytics.py
description: "Compute total score moving average and trend."
```

### `compute_streaks`

```yaml
name: compute_streaks
kind: function
path: src/amatelier/engine/analytics.py
description: "Detect scoring streaks and personal records."
```

### `detect_phase`

```yaml
name: detect_phase
kind: function
path: src/amatelier/engine/analytics.py
description: "Detect which development phase an agent is in."
```

### `identify_strengths_weaknesses`

```yaml
name: identify_strengths_weaknesses
kind: function
path: src/amatelier/engine/analytics.py
description: "Identify strongest and weakest dimensions over recent window."
```

### `budget_analytics`

```yaml
name: budget_analytics
kind: function
path: src/amatelier/engine/analytics.py
description: "Analyze budget usage patterns across roundtables."
```

### `judge_redirect_analytics`

```yaml
name: judge_redirect_analytics
kind: function
path: src/amatelier/engine/analytics.py
description: "Count Judge redirects per roundtable."
```

### `therapist_analytics`

```yaml
name: therapist_analytics
kind: function
path: src/amatelier/engine/analytics.py
description: "Aggregate therapist session outcomes."
```

### `economy_analytics`

```yaml
name: economy_analytics
kind: function
path: src/amatelier/engine/analytics.py
description: "Spark economy analytics: earnings, spending, ROI."
```

### `save_leaderboard_snapshot`

```yaml
name: save_leaderboard_snapshot
kind: function
path: src/amatelier/engine/analytics.py
description: "Save current leaderboard as a timestamped snapshot."
```

### `rank_trajectory`

```yaml
name: rank_trajectory
kind: function
path: src/amatelier/engine/analytics.py
description: "Get rank trajectory from leaderboard history."
```

### `engagement_matrix`

```yaml
name: engagement_matrix
kind: function
path: src/amatelier/engine/analytics.py
description: "Build a matrix of who references whom across roundtables."
```

### `agent_report`

```yaml
name: agent_report
kind: function
path: src/amatelier/engine/analytics.py
description: "Generate a comprehensive growth report for one agent."
```

### `format_report_text`

```yaml
name: format_report_text
kind: function
path: src/amatelier/engine/analytics.py
description: "Format a growth report as readable text for the Therapist or user."
```

### `economy_overview`

```yaml
name: economy_overview
kind: function
path: src/amatelier/engine/analytics.py
description: "Generate a system-wide Spark Economy overview."
```

### `update_agent_analytics`

```yaml
name: update_agent_analytics
kind: function
path: src/amatelier/engine/analytics.py
description: "Compute and persist analytics fields into metrics.json."
```

### `update_all_analytics`

```yaml
name: update_all_analytics
kind: function
path: src/amatelier/engine/analytics.py
description: "Update analytics for all workers."
```

### `distill_one`

```yaml
name: distill_one
kind: function
path: src/amatelier/engine/backfill_distill.py
description: "Run the Sonnet distiller on a single digest. Returns result dict."
```

### `main`

```yaml
name: main
kind: function
path: src/amatelier/engine/backfill_distill.py
description: ""
```

### `main`

```yaml
name: main
kind: function
path: src/amatelier/engine/classify_concepts.py
description: ""
```

### `load_agent_context`

```yaml
name: load_agent_context
kind: function
path: src/amatelier/engine/claude_agent.py
description: "Load the agent's full context: CLAUDE.md + MEMORY.md + metrics + skills."
```

### `call_claude`

```yaml
name: call_claude
kind: function
path: src/amatelier/engine/claude_agent.py
description: "Call Claude."
```

### `run_agent`

```yaml
name: run_agent
kind: function
path: src/amatelier/engine/claude_agent.py
description: "Main loop: wait for call → respond → repeat until roundtable closes."
```

### `get_db`

```yaml
name: get_db
kind: function
path: src/amatelier/engine/db.py
description: ""
```

### `get_active_roundtable`

```yaml
name: get_active_roundtable
kind: function
path: src/amatelier/engine/db.py
description: ""
```

### `listen`

```yaml
name: listen
kind: function
path: src/amatelier/engine/db.py
description: "Read all messages since last read. Uses context manager for safe cleanup."
```

### `speak`

```yaml
name: speak
kind: function
path: src/amatelier/engine/db.py
description: "Post a message to the roundtable. Uses context manager for safe cleanup."
```

### `is_roundtable_open`

```yaml
name: is_roundtable_open
kind: function
path: src/amatelier/engine/db.py
description: ""
```

### `init_read_cursor`

```yaml
name: init_read_cursor
kind: function
path: src/amatelier/engine/db.py
description: "Initialize a read cursor for an agent joining a roundtable."
```

### `recall`

```yaml
name: recall
kind: function
path: src/amatelier/engine/db.py
description: "Retrieve specific transcript segments from the current roundtable."
```

### `build_transcript_index`

```yaml
name: build_transcript_index
kind: function
path: src/amatelier/engine/db.py
description: "Build a compact one-line-per-contribution index of the transcript."
```

### `load_index`

```yaml
name: load_index
kind: function
path: src/amatelier/engine/distiller.py
description: ""
```

### `save_index`

```yaml
name: save_index
kind: function
path: src/amatelier/engine/distiller.py
description: ""
```

### `create_skill_entry`

```yaml
name: create_skill_entry
kind: function
path: src/amatelier/engine/distiller.py
description: "Create a structured skill entry. Returns None if duplicate or fails JUDGE gate."
```

### `save_skill_to_agent`

```yaml
name: save_skill_to_agent
kind: function
path: src/amatelier/engine/distiller.py
description: "Save a skill to an agent's local skills directory."
```

### `promote_to_shared`

```yaml
name: promote_to_shared
kind: function
path: src/amatelier/engine/distiller.py
description: "Promote a skill to the shared skill store."
```

### `search_shared_skills`

```yaml
name: search_shared_skills
kind: function
path: src/amatelier/engine/distiller.py
description: "Simple keyword search against shared skill index."
```

### `list_agent_skills`

```yaml
name: list_agent_skills
kind: function
path: src/amatelier/engine/distiller.py
description: "List all skills an agent has accumulated."
```

### `load_behaviors`

```yaml
name: load_behaviors
kind: function
path: src/amatelier/engine/evolver.py
description: "Load structured behavior metadata."
```

### `save_behaviors`

```yaml
name: save_behaviors
kind: function
path: src/amatelier/engine/evolver.py
description: ""
```

### `confirm_behavior`

```yaml
name: confirm_behavior
kind: function
path: src/amatelier/engine/evolver.py
description: "Confirm a behavior was useful — resets confidence to 1.0."
```

### `decay_behaviors`

```yaml
name: decay_behaviors
kind: function
path: src/amatelier/engine/evolver.py
description: "Apply decay to all behaviors for one RT cycle."
```

### `get_behavior_decay_summary`

```yaml
name: get_behavior_decay_summary
kind: function
path: src/amatelier/engine/evolver.py
description: "Build a summary string for Therapist injection."
```

### `read_claude_md`

```yaml
name: read_claude_md
kind: function
path: src/amatelier/engine/evolver.py
description: ""
```

### `write_claude_md`

```yaml
name: write_claude_md
kind: function
path: src/amatelier/engine/evolver.py
description: ""
```

### `append_to_memory`

```yaml
name: append_to_memory
kind: function
path: src/amatelier/engine/evolver.py
description: "Dual-write: structured MEMORY.json (primary) + legacy MEMORY.md (backup)."
```

### `add_learned_behavior`

```yaml
name: add_learned_behavior
kind: function
path: src/amatelier/engine/evolver.py
description: "Add a learned behavior to CLAUDE.md + behaviors.json. Skips duplicates (>70% word overlap)."
```

### `remove_learned_behavior`

```yaml
name: remove_learned_behavior
kind: function
path: src/amatelier/engine/evolver.py
description: "Remove a learned behavior from CLAUDE.md + behaviors.json by fuzzy match."
```

### `update_emerging_trait`

```yaml
name: update_emerging_trait
kind: function
path: src/amatelier/engine/evolver.py
description: "Update the Emerging Traits section in an agent's CLAUDE.md."
```

### `sync_skills_owned`

```yaml
name: sync_skills_owned
kind: function
path: src/amatelier/engine/evolver.py
description: "Update the ## Skills Owned section in an agent's CLAUDE.md from the ledger."
```

### `load_agent_context`

```yaml
name: load_agent_context
kind: function
path: src/amatelier/engine/gemini_agent.py
description: ""
```

### `run_agent`

```yaml
name: run_agent
kind: function
path: src/amatelier/engine/gemini_agent.py
description: "Wait for call -> respond -> repeat until roundtable closes."
```

### `get_model_name`

```yaml
name: get_model_name
kind: function
path: src/amatelier/engine/gemini_client.py
description: "Read Gemini model name from config.json."
```

### `call_gemini`

```yaml
name: call_gemini
kind: function
path: src/amatelier/engine/gemini_client.py
description: "Call Gemini with retries, rate limiting, and detailed error logging."
```

### `judge_score`

```yaml
name: judge_score
kind: function
path: src/amatelier/engine/judge_scorer.py
description: "Score all workers using Sonnet Judge. Returns results dict."
```

### `load_config`

```yaml
name: load_config
kind: function
path: src/amatelier/engine/roundtable_runner.py
description: ""
```

### `resolve_agent_model`

```yaml
name: resolve_agent_model
kind: function
path: src/amatelier/engine/roundtable_runner.py
description: "Read agent's model from config. Upgrades are request-based, not automatic."
```

### `db_cmd`

```yaml
name: db_cmd
kind: function
path: src/amatelier/engine/roundtable_runner.py
description: "Run a db_client.py command and return parsed JSON."
```

### `get_latest_messages`

```yaml
name: get_latest_messages
kind: function
path: src/amatelier/engine/roundtable_runner.py
description: "Listen as the runner to get new messages."
```

### `wait_for_single_speaker`

```yaml
name: wait_for_single_speaker
kind: function
path: src/amatelier/engine/roundtable_runner.py
description: "Wait for a single agent to post. Returns (spoke, all_new_messages)."
```

### `is_pass`

```yaml
name: is_pass
kind: function
path: src/amatelier/engine/roundtable_runner.py
description: "Check if the speaker's MOST RECENT message is a PASS."
```

### `check_convergence`

```yaml
name: check_convergence
kind: function
path: src/amatelier/engine/roundtable_runner.py
description: "Check if Judge posted a CONVERGED signal."
```

### `build_digest`

```yaml
name: build_digest
kind: function
path: src/amatelier/engine/roundtable_runner.py
description: "Build a structured digest from the transcript."
```

### `format_budget_status`

```yaml
name: format_budget_status
kind: function
path: src/amatelier/engine/roundtable_runner.py
description: "Format budget as a readable string."
```

### `run_roundtable`

```yaml
name: run_roundtable
kind: function
path: src/amatelier/engine/roundtable_runner.py
description: "Run a complete roundtable with structured debate and return the digest."
```

### `format_digest_summary`

```yaml
name: format_digest_summary
kind: function
path: src/amatelier/engine/roundtable_runner.py
description: "Format digest as human-readable summary for Admin."
```

### `load_config`

```yaml
name: load_config
kind: function
path: src/amatelier/engine/scorer.py
description: ""
```

### `load_metrics`

```yaml
name: load_metrics
kind: function
path: src/amatelier/engine/scorer.py
description: ""
```

### `save_metrics`

```yaml
name: save_metrics
kind: function
path: src/amatelier/engine/scorer.py
description: "Write metrics.json with file locking to prevent concurrent-write corruption."
```

### `score_agent`

```yaml
name: score_agent
kind: function
path: src/amatelier/engine/scorer.py
description: "Score an agent's roundtable contribution and update their metrics."
```

### `deduct_entry_fee`

```yaml
name: deduct_entry_fee
kind: function
path: src/amatelier/engine/scorer.py
description: "Deduct flat RT entry fee from agent's spark balance."
```

### `get_leaderboard`

```yaml
name: get_leaderboard
kind: function
path: src/amatelier/engine/scorer.py
description: "Get ranked leaderboard of all workers."
```

### `promote_tier`

```yaml
name: promote_tier
kind: function
path: src/amatelier/engine/scorer.py
description: "Agent purchases a tier promotion with sparks."
```

### `check_self_determined`

```yaml
name: check_self_determined
kind: function
path: src/amatelier/engine/scorer.py
description: "Check if an agent has earned the right to choose their own evolution path."
```

### `pitch_venture`

```yaml
name: pitch_venture
kind: function
path: src/amatelier/engine/scorer.py
description: "Agent stakes sparks on an experimental idea."
```

### `resolve_venture`

```yaml
name: resolve_venture
kind: function
path: src/amatelier/engine/scorer.py
description: "Admin resolves a venture — success awards multiplier, failure loses stake."
```

### `award_gate_bonus`

```yaml
name: award_gate_bonus
kind: function
path: src/amatelier/engine/scorer.py
description: "Judge awards a gate bonus for an exceptional contribution."
```

### `award_rt_outcome_bonus`

```yaml
name: award_rt_outcome_bonus
kind: function
path: src/amatelier/engine/scorer.py
description: "Award bonus sparks to agents whose RT proposal was implemented by the user."
```

### `get_spark_balances`

```yaml
name: get_spark_balances
kind: function
path: src/amatelier/engine/scorer.py
description: "Get spark balances and pending ventures for all workers."
```

### `compute_variance_flags`

```yaml
name: compute_variance_flags
kind: function
path: src/amatelier/engine/scorer.py
description: "Detect byzantine scoring patterns — flat low or erratic across axes."
```

### `check_underperformers`

```yaml
name: check_underperformers
kind: function
path: src/amatelier/engine/scorer.py
description: "Identify agents at risk of relegation (3 consecutive net-negative RTs)."
```

### `parse_requests`

```yaml
name: parse_requests
kind: function
path: src/amatelier/engine/steward_dispatch.py
description: "Extract all [[request: ...]] blocks from an agent's message."
```

### `strip_requests`

```yaml
name: strip_requests
kind: function
path: src/amatelier/engine/steward_dispatch.py
description: "Return the message with [[request:]] blocks removed (for transcript)."
```

### `StewardBudget`

```yaml
name: StewardBudget
kind: class
path: src/amatelier/engine/steward_dispatch.py
description: "Per-agent, per-RT request budget."
```

### `load_registered_files`

```yaml
name: load_registered_files
kind: function
path: src/amatelier/engine/steward_dispatch.py
description: "Parse the briefing for a '## Steward-Registered Files' section."
```

### `resolve_file`

```yaml
name: resolve_file
kind: function
path: src/amatelier/engine/steward_dispatch.py
description: "Check if a file path (or glob) is in the registered list."
```

### `try_deterministic`

```yaml
name: try_deterministic
kind: function
path: src/amatelier/engine/steward_dispatch.py
description: "Attempt to handle the request without an LLM."
```

### `spawn_steward_subagent`

```yaml
name: spawn_steward_subagent
kind: function
path: src/amatelier/engine/steward_dispatch.py
description: "Spawn an ephemeral Claude subagent with file tools to execute a lookup."
```

### `StewardTask`

```yaml
name: StewardTask
kind: class
path: src/amatelier/engine/steward_dispatch.py
description: "A single async Steward lookup running in a background thread."
```

### `format_result`

```yaml
name: format_result
kind: function
path: src/amatelier/engine/steward_dispatch.py
description: "Format a Steward result for injection into the debate context."
```

### `StewardLog`

```yaml
name: StewardLog
kind: class
path: src/amatelier/engine/steward_dispatch.py
description: "Tracks all Steward requests and results for an RT."
```

### `remaining`

```yaml
name: remaining
kind: function
path: src/amatelier/engine/steward_dispatch.py
description: ""
```

### `spend`

```yaml
name: spend
kind: function
path: src/amatelier/engine/steward_dispatch.py
description: "Deduct 1 from agent's budget. Returns False if out of budget."
```

### `status`

```yaml
name: status
kind: function
path: src/amatelier/engine/steward_dispatch.py
description: ""
```

### `log`

```yaml
name: log
kind: function
path: src/amatelier/engine/steward_dispatch.py
description: ""
```

### `wait`

```yaml
name: wait
kind: function
path: src/amatelier/engine/steward_dispatch.py
description: ""
```

### `record`

```yaml
name: record
kind: function
path: src/amatelier/engine/steward_dispatch.py
description: ""
```

### `save`

```yaml
name: save
kind: function
path: src/amatelier/engine/steward_dispatch.py
description: ""
```

### `entries`

```yaml
name: entries
kind: function
path: src/amatelier/engine/steward_dispatch.py
description: ""
```

### `load_catalog`

```yaml
name: load_catalog
kind: function
path: src/amatelier/engine/store.py
description: ""
```

### `load_ledger`

```yaml
name: load_ledger
kind: function
path: src/amatelier/engine/store.py
description: ""
```

### `save_ledger`

```yaml
name: save_ledger
kind: function
path: src/amatelier/engine/store.py
description: ""
```

### `find_item`

```yaml
name: find_item
kind: function
path: src/amatelier/engine/store.py
description: "Find an item by ID across all categories. Returns (item, category_key)."
```

### `list_catalog`

```yaml
name: list_catalog
kind: function
path: src/amatelier/engine/store.py
description: "Format catalog as readable text."
```

### `what_can_afford`

```yaml
name: what_can_afford
kind: function
path: src/amatelier/engine/store.py
description: "Show what an agent can afford right now."
```

### `purchase`

```yaml
name: purchase
kind: function
path: src/amatelier/engine/store.py
description: "Process a store purchase. Deducts sparks, records in ledger."
```

### `submit_request`

```yaml
name: submit_request
kind: function
path: src/amatelier/engine/store.py
description: "Submit a public or private store request."
```

### `get_pending_boosts`

```yaml
name: get_pending_boosts
kind: function
path: src/amatelier/engine/store.py
description: "Get unconsumed boosts for an agent (purchased but not yet used in an RT)."
```

### `consume_boost`

```yaml
name: consume_boost
kind: function
path: src/amatelier/engine/store.py
description: "Mark a boost as consumed after use in a roundtable. Returns True if consumed."
```

### `apply_boosts_for_rt`

```yaml
name: apply_boosts_for_rt
kind: function
path: src/amatelier/engine/store.py
description: "Check all workers for pending boosts. Returns {agent: {effect_type: value}}."
```

### `consume_boosts_after_rt`

```yaml
name: consume_boosts_after_rt
kind: function
path: src/amatelier/engine/store.py
description: "Consume all pending boosts for workers after an RT completes."
```

### `age_bulletin_requests`

```yaml
name: age_bulletin_requests
kind: function
path: src/amatelier/engine/store.py
description: "Increment rt_count on all open requests. Returns requests that hit 3 (expired)."
```

### `fulfill_request`

```yaml
name: fulfill_request
kind: function
path: src/amatelier/engine/store.py
description: "An agent fulfills a public request by creating a marketplace skill."
```

### `get_open_requests`

```yaml
name: get_open_requests
kind: function
path: src/amatelier/engine/store.py
description: "Return all open bulletin board requests with their index. Used by Admin during curation."
```

### `admin_list_skill`

```yaml
name: admin_list_skill
kind: function
path: src/amatelier/engine/store.py
description: "Admin lists a curated skill in the store catalog."
```

### `admin_apply_private_skill`

```yaml
name: admin_apply_private_skill
kind: function
path: src/amatelier/engine/store.py
description: "Admin applies a privately requested skill directly to an agent."
```

### `retire_skill`

```yaml
name: retire_skill
kind: function
path: src/amatelier/engine/store.py
description: "Retire a skill an agent owns. Marks it as retired in ledger, removes skill file."
```

### `get_owned_skills`

```yaml
name: get_owned_skills
kind: function
path: src/amatelier/engine/store.py
description: "Get all permanent skills an agent owns (from ledger). Excludes retired."
```

### `inventory`

```yaml
name: inventory
kind: function
path: src/amatelier/engine/store.py
description: "Get everything an agent owns."
```

### `purchase_history`

```yaml
name: purchase_history
kind: function
path: src/amatelier/engine/store.py
description: "Format purchase history as readable text."
```

### `bulletin_board`

```yaml
name: bulletin_board
kind: function
path: src/amatelier/engine/store.py
description: "Show public requests with aging and fulfillment status."
```

### `run_session`

```yaml
name: run_session
kind: function
path: src/amatelier/engine/therapist.py
description: "Run a single Therapist session with one agent."
```

### `run_therapist`

```yaml
name: run_therapist
kind: function
path: src/amatelier/engine/therapist.py
description: "Run Therapist sessions for all agents in a roundtable."
```

### `BackendUnavailable`

```yaml
name: BackendUnavailable
kind: class
path: src/amatelier/llm_backend.py
description: "Raised when no backend can satisfy the current environment."
```

### `Completion`

```yaml
name: Completion
kind: class
path: src/amatelier/llm_backend.py
description: ""
```

### `LLMBackend`

```yaml
name: LLMBackend
kind: class
path: src/amatelier/llm_backend.py
description: ""
```

### `ClaudeCLIBackend`

```yaml
name: ClaudeCLIBackend
kind: class
path: src/amatelier/llm_backend.py
description: ""
```

### `AnthropicSDKBackend`

```yaml
name: AnthropicSDKBackend
kind: class
path: src/amatelier/llm_backend.py
description: ""
```

### `OpenAICompatBackend`

```yaml
name: OpenAICompatBackend
kind: class
path: src/amatelier/llm_backend.py
description: ""
```

### `describe_environment`

```yaml
name: describe_environment
kind: function
path: src/amatelier/llm_backend.py
description: "Diagnostic snapshot — used by ``amatelier config``."
```

### `resolve_mode`

```yaml
name: resolve_mode
kind: function
path: src/amatelier/llm_backend.py
description: "Resolve the mode to use."
```

### `get_backend`

```yaml
name: get_backend
kind: function
path: src/amatelier/llm_backend.py
description: "Return a singleton backend instance configured for the current env."
```

### `call_claude`

```yaml
name: call_claude
kind: function
path: src/amatelier/llm_backend.py
description: "Backward-compatible shim matching the original claude_agent.call_claude API."
```

### `complete`

```yaml
name: complete
kind: function
path: src/amatelier/llm_backend.py
description: ""
```

### `available`

```yaml
name: available
kind: function
path: src/amatelier/llm_backend.py
description: ""
```

### `complete`

```yaml
name: complete
kind: function
path: src/amatelier/llm_backend.py
description: ""
```

### `available`

```yaml
name: available
kind: function
path: src/amatelier/llm_backend.py
description: ""
```

### `complete`

```yaml
name: complete
kind: function
path: src/amatelier/llm_backend.py
description: ""
```

### `available`

```yaml
name: available
kind: function
path: src/amatelier/llm_backend.py
description: ""
```

### `complete`

```yaml
name: complete
kind: function
path: src/amatelier/llm_backend.py
description: ""
```

### `bundled_assets_dir`

```yaml
name: bundled_assets_dir
kind: function
path: src/amatelier/paths.py
description: "The read-only bundled-assets root. Inside the installed package."
```

### `bundled_docs_dir`

```yaml
name: bundled_docs_dir
kind: function
path: src/amatelier/paths.py
description: "The bundled human docs (Diátaxis tree)."
```

### `bundled_agent_dir`

```yaml
name: bundled_agent_dir
kind: function
path: src/amatelier/paths.py
description: "Seed persona directory for an agent (ships in wheel, read-only)."
```

### `bundled_store_catalog`

```yaml
name: bundled_store_catalog
kind: function
path: src/amatelier/paths.py
description: "Default skill template catalog."
```

### `bundled_config`

```yaml
name: bundled_config
kind: function
path: src/amatelier/paths.py
description: "Default config.json."
```

### `user_data_dir`

```yaml
name: user_data_dir
kind: function
path: src/amatelier/paths.py
description: "The user-writable root for runtime state."
```

### `user_agent_dir`

```yaml
name: user_agent_dir
kind: function
path: src/amatelier/paths.py
description: "Per-agent mutable state (MEMORY, metrics, behaviors, sessions, skills)."
```

### `user_db_path`

```yaml
name: user_db_path
kind: function
path: src/amatelier/paths.py
description: "SQLite database for the roundtable chat."
```

### `user_logs_dir`

```yaml
name: user_logs_dir
kind: function
path: src/amatelier/paths.py
description: "Runtime logs (gemini_errors, runner logs, etc.)."
```

### `user_digest_dir`

```yaml
name: user_digest_dir
kind: function
path: src/amatelier/paths.py
description: "Where ``digest-<rt_id>.json`` and related transcripts land."
```

### `user_briefing_dir`

```yaml
name: user_briefing_dir
kind: function
path: src/amatelier/paths.py
description: "Where user-authored briefing-*.md files are expected."
```

### `user_store_ledger`

```yaml
name: user_store_ledger
kind: function
path: src/amatelier/paths.py
description: "Spark economy ledger (evolves as agents earn and spend)."
```

### `user_novel_concepts`

```yaml
name: user_novel_concepts
kind: function
path: src/amatelier/paths.py
description: "DERIVE skill concepts accumulated across roundtables."
```

### `user_shared_skills_index`

```yaml
name: user_shared_skills_index
kind: function
path: src/amatelier/paths.py
description: "Curated shared skills from Admin distillation."
```

### `user_config_override`

```yaml
name: user_config_override
kind: function
path: src/amatelier/paths.py
description: "Optional user-level override for config.json."
```

### `ensure_user_data`

```yaml
name: ensure_user_data
kind: function
path: src/amatelier/paths.py
description: "Create the user-data tree on first use."
```

### `get_db`

```yaml
name: get_db
kind: function
path: src/amatelier/roundtable-server/db_client.py
description: ""
```

### `get_active_rt`

```yaml
name: get_active_rt
kind: function
path: src/amatelier/roundtable-server/db_client.py
description: ""
```

### `cmd_open`

```yaml
name: cmd_open
kind: function
path: src/amatelier/roundtable-server/db_client.py
description: ""
```

### `cmd_join`

```yaml
name: cmd_join
kind: function
path: src/amatelier/roundtable-server/db_client.py
description: ""
```

### `cmd_speak`

```yaml
name: cmd_speak
kind: function
path: src/amatelier/roundtable-server/db_client.py
description: ""
```

### `cmd_listen`

```yaml
name: cmd_listen
kind: function
path: src/amatelier/roundtable-server/db_client.py
description: ""
```

### `cmd_status`

```yaml
name: cmd_status
kind: function
path: src/amatelier/roundtable-server/db_client.py
description: ""
```

### `cmd_close`

```yaml
name: cmd_close
kind: function
path: src/amatelier/roundtable-server/db_client.py
description: ""
```

### `cmd_cut`

```yaml
name: cmd_cut
kind: function
path: src/amatelier/roundtable-server/db_client.py
description: ""
```

### `cmd_recall`

```yaml
name: cmd_recall
kind: function
path: src/amatelier/roundtable-server/db_client.py
description: "Retrieve specific transcript segments from the active roundtable."
```

### `cmd_index`

```yaml
name: cmd_index
kind: function
path: src/amatelier/roundtable-server/db_client.py
description: "Show the transcript index — compact one-line-per-contribution view."
```

### `cmd_transcript`

```yaml
name: cmd_transcript
kind: function
path: src/amatelier/roundtable-server/db_client.py
description: ""
```

### `get_db`

```yaml
name: get_db
kind: function
path: src/amatelier/roundtable-server/server.py
description: "Get a WAL-mode SQLite connection."
```

### `init_db`

```yaml
name: init_db
kind: function
path: src/amatelier/roundtable-server/server.py
description: "Create tables if they don't exist."
```

### `roundtable_open`

```yaml
name: roundtable_open
kind: function
path: src/amatelier/roundtable-server/server.py
description: "Open a new roundtable discussion."
```

### `roundtable_close`

```yaml
name: roundtable_close
kind: function
path: src/amatelier/roundtable-server/server.py
description: "Close the active roundtable and return the full transcript."
```

### `roundtable_cut`

```yaml
name: roundtable_cut
kind: function
path: src/amatelier/roundtable-server/server.py
description: "Force-end the active roundtable (consensus, repetition, token ceiling, etc)."
```

### `roundtable_join`

```yaml
name: roundtable_join
kind: function
path: src/amatelier/roundtable-server/server.py
description: "Join the active roundtable discussion."
```

### `roundtable_speak`

```yaml
name: roundtable_speak
kind: function
path: src/amatelier/roundtable-server/server.py
description: "Post a message to the active roundtable discussion."
```

### `roundtable_listen`

```yaml
name: roundtable_listen
kind: function
path: src/amatelier/roundtable-server/server.py
description: "Read all messages since your last read. First call returns full history."
```

### `roundtable_leave`

```yaml
name: roundtable_leave
kind: function
path: src/amatelier/roundtable-server/server.py
description: "Leave the active roundtable."
```

### `roundtable_review`

```yaml
name: roundtable_review
kind: function
path: src/amatelier/roundtable-server/server.py
description: "Read the full transcript of a completed roundtable."
```

### `roundtable_history`

```yaml
name: roundtable_history
kind: function
path: src/amatelier/roundtable-server/server.py
description: "List past roundtables."
```

### `roundtable_status`

```yaml
name: roundtable_status
kind: function
path: src/amatelier/roundtable-server/server.py
description: "Get the current roundtable status — who's in, message count, is it open."
```

### `main`

```yaml
name: main
kind: function
path: src/amatelier/roundtable-server/server.py
description: ""
```

### `result`

```yaml
name: result
kind: function
path: src/amatelier/roundtable-server/test_roundtable.py
description: ""
```

### `get_db`

```yaml
name: get_db
kind: function
path: src/amatelier/roundtable-server/test_roundtable.py
description: ""
```

### `test_db_exists`

```yaml
name: test_db_exists
kind: function
path: src/amatelier/roundtable-server/test_roundtable.py
description: ""
```

### `test_open_roundtable`

```yaml
name: test_open_roundtable
kind: function
path: src/amatelier/roundtable-server/test_roundtable.py
description: ""
```

### `test_join`

```yaml
name: test_join
kind: function
path: src/amatelier/roundtable-server/test_roundtable.py
description: ""
```

### `test_speak`

```yaml
name: test_speak
kind: function
path: src/amatelier/roundtable-server/test_roundtable.py
description: ""
```

### `test_listen`

```yaml
name: test_listen
kind: function
path: src/amatelier/roundtable-server/test_roundtable.py
description: ""
```

### `test_close`

```yaml
name: test_close
kind: function
path: src/amatelier/roundtable-server/test_roundtable.py
description: ""
```

### `test_cut`

```yaml
name: test_cut
kind: function
path: src/amatelier/roundtable-server/test_roundtable.py
description: ""
```

### `test_concurrent_writers`

```yaml
name: test_concurrent_writers
kind: function
path: src/amatelier/roundtable-server/test_roundtable.py
description: ""
```

### `test_gemini_agent_loadable`

```yaml
name: test_gemini_agent_loadable
kind: function
path: src/amatelier/roundtable-server/test_roundtable.py
description: ""
```

### `cleanup`

```yaml
name: cleanup
kind: function
path: src/amatelier/roundtable-server/test_roundtable.py
description: "Remove test roundtables."
```

### `agent_color`

```yaml
name: agent_color
kind: function
path: src/amatelier/tools/watch_roundtable.py
description: ""
```

### `agent_role`

```yaml
name: agent_role
kind: function
path: src/amatelier/tools/watch_roundtable.py
description: ""
```

### `is_gate`

```yaml
name: is_gate
kind: function
path: src/amatelier/tools/watch_roundtable.py
description: ""
```

### `RichWatcher`

```yaml
name: RichWatcher
kind: class
path: src/amatelier/tools/watch_roundtable.py
description: ""
```

### `plain_watch`

```yaml
name: plain_watch
kind: function
path: src/amatelier/tools/watch_roundtable.py
description: "Degraded renderer used when rich isn't importable."
```

### `main`

```yaml
name: main
kind: function
path: src/amatelier/tools/watch_roundtable.py
description: ""
```

### `run`

```yaml
name: run
kind: function
path: src/amatelier/tools/watch_roundtable.py
description: ""
```

================================================================================
FILE: llm/SCHEMA.md
================================================================================

# Schema

> **Generated.** Do not hand-edit. Regenerate with `python scripts/regen_llm.py`.


## Config keys

_No pydantic models or JSON schemas detected in this repo. Populate by implementing the schema-walking logic in `find_schemas()` when pydantic/dataclass usage appears._

================================================================================
FILE: llm/WORKFLOWS.md
================================================================================

# Workflows

> Turn-by-turn orchestration paths. Hand-written. Answers the question: "this command ran — what happened, step by step, and which module.function owns each step?"

## Conventions

- Each workflow is a named section
- Steps are numbered, expressed as YAML
- Each step names the module:function responsible
- Inputs and outputs are explicit
- Failure modes listed at the end of each workflow

## Workflow: amatelier roundtable (happy path)

Trigger: `amatelier roundtable --topic T --briefing B [--workers CSV] [--max-rounds N] [--skip-naomi] [--budget N] [--speaker-timeout S] [--skip-post] [--summary]`

```yaml
- step: 1
  actor: amatelier.cli:main
  action: Reconfigure stdout/stderr to utf-8; dispatch on argv[0]
  inputs: sys.argv[1:]
  outputs: dispatch decision
- step: 2
  actor: amatelier.cli:_run_engine_module
  action: sys.argv = ["roundtable_runner", ...rest]; runpy.run_module("roundtable_runner", run_name="__main__", alter_sys=True)
  note: The engine module ships as a flat-import module because __init__.py inserts src/amatelier/engine into sys.path
- step: 3
  actor: amatelier.engine.roundtable_runner (module __main__ block)
  action: argparse parses flags; calls run_roundtable(topic, briefing_path, workers, max_rounds, skip_naomi, speaker_timeout, budget_per_agent, skip_post)
- step: 4
  actor: amatelier.engine.roundtable_runner:run_roundtable
  action: load_config() reads bundled config.json; resolve default_workers if --workers not given; resolve max_rounds default
- step: 5
  actor: amatelier.engine.roundtable_runner:run_roundtable
  action: Resolve briefing file — first try SUITE_ROOT/roundtable-server/<briefing>, else treat as absolute path; read text; FileNotFoundError if missing
- step: 6
  actor: amatelier.engine.steward_dispatch:load_registered_files
  action: Parse "## Steward-Registered Files" section from briefing markdown; produce list of absolute paths. If empty, steward_enabled flips to False for this RT
- step: 7
  actor: amatelier.engine.store:apply_boosts_for_rt
  action: Read store/ledger.json for each worker; apply consumable effects (e.g. +extra_floor_turns to budget dict)
- step: 8
  actor: amatelier.engine.evolver:sync_skills_owned
  action: Per worker — diff spark_ledger category=purchase entries against MEMORY.json.skills_owned; refresh
- step: 9
  actor: amatelier.engine.agent_memory:generate_session_bridge
  action: Per worker — emit "last time you..." context block from prior session summary; stored for next agent-context assembly
- step: 10
  actor: amatelier.engine.db (via db_client.py subprocess)
  action: db_cmd("open", topic, participants_csv) — INSERT INTO roundtables; returns rt_id (hex). Runner joins as "runner"
- step: 11
  actor: amatelier.engine.db (via db_client.py subprocess)
  action: db_cmd("speak", "runner", "BRIEFING:\n\n<text>") — posts briefing to chat
- step: 12
  actor: amatelier.engine.roundtable_runner:run_roundtable
  action: For each worker — resolve_agent_model() reads config.json team.workers.<name>.model; determines entry-fee tier (haiku/flash/sonnet/opus)
- step: 13
  actor: amatelier.engine.scorer (via subprocess "deduct-fee")
  action: Insert spark_ledger row with negative amount = entry fee; category=fee
- step: 14
  actor: amatelier.engine.roundtable_runner._launch_claude / _launch_gemini
  action: subprocess.Popen per worker — claude_agent.py or gemini_agent.py; cwd=WORKSPACE_ROOT; env includes PYTHONIOENCODING=utf-8 and loaded .env vars. Also launch judge (Claude sonnet). Sleep 5s for connection
- step: 15
  actor: amatelier.engine.roundtable_runner:run_roundtable (Research Window — optional)
  action: If steward_enabled and registered_files — broadcast RESEARCH WINDOW signal; each worker speaks once with up to 3 [[request:]] tags; fire StewardTask per request (free, budget-exempt); await all; inject results via db_cmd("speak", "runner", format_result(...))
- step: 16
  actor: amatelier.engine.roundtable_runner:run_roundtable (speaking-order setup)
  action: random.shuffle(all_workers); _resolve_first_speaker() scans store/ledger.json for pending first-speaker purchases; highest-rank bidder wins; winner inserted at index 0, losers refunded into metrics.json
- step: 17
  actor: amatelier.engine.roundtable_runner:run_roundtable (round loop, 1..max_rounds)
  action: For each round — post "ROUND N: begin\nBUDGET STATUS: <...>"
- step: 18
  actor: amatelier.engine.roundtable_runner (SPEAK PHASE)
  action: Post "--- SPEAK PHASE (Round N) ---". Process queue — for each agent, _call_speaker posts "YOUR TURN: <name> — SPEAK" and wait_for_single_speaker polls DB until that agent writes or speaker_timeout (default 200s). Parse returned message for [[request:]] tags
- step: 19
  actor: amatelier.engine.steward_dispatch (if request tag present)
  action: steward_budget.spend() decrements; StewardTask started in background thread; agent appended to back of speak_queue (deferred). Task result polled before subsequent agents speak; INTERMISSION fires if entire queue is deferred; post-phase all pending tasks drained
- step: 20
  actor: amatelier.engine.roundtable_runner (stability measurement)
  action: If round_num > 1, compute per-agent Jaccard similarity between this round's SPEAK and prior round's SPEAK; log stability_scores[round_num]
- step: 21
  actor: amatelier.engine.roundtable_runner (REBUTTAL PHASE)
  action: Post "--- REBUTTAL PHASE ---"; iterate speaking_order in reverse; _call_speaker each. No [[request:]] deferral in rebuttal phase
- step: 22
  actor: amatelier.engine.roundtable_runner (JUDGE GATE)
  action: Post "--- JUDGE GATE ---" with instruction to emit CONVERGED:<reason> or CONTINUE:<gap>. wait_for_single_speaker("judge"). check_convergence scans for CONVERGED prefix; if found, break round loop
- step: 23
  actor: amatelier.engine.roundtable_runner (FLOOR PHASE)
  action: Gather floor_eligible = workers with budget > 0. Iterate — _call_speaker. is_pass() detects PASS response; contributions decrement budget[agent] by 1 and mark eligible for another floor turn while budget remains. Continue until all pass or all budgets exhausted
- step: 24
  actor: amatelier.engine.roundtable_runner (round health audit)
  action: Count alive agent_procs; count "timed out"/"did not respond" runner messages this round. Majority-dead → abort
- step: 25
  actor: amatelier.engine.roundtable_runner (Gemini refresh, conditional)
  action: At round == gemini_refresh_round (default 5, from config.roundtable.gemini_refresh_round), terminate and relaunch naomi subprocess to reset API rate-limit state
- step: 26
  actor: amatelier.engine.db (via db_client.py subprocess)
  action: After round loop completes — db_cmd("close"); RT status=closed
- step: 27
  actor: amatelier.engine.roundtable_runner:build_digest
  action: Assemble digest dict — topic, rt_id, rounds, total_messages, contributions (count by agent), final_positions (last message by agent), converged, convergence_reason, judge_interventions, budget_usage, stability_scores
- step: 28
  actor: amatelier.engine.judge_scorer:judge_score (invoked by runner)
  action: Judge LLM call with full transcript; effort=max sonnet; emits per-agent scores on 4 axes (0/1/2/3/10). Persist to scores table
- step: 29
  actor: amatelier.engine.roundtable_runner:_process_gate_bonuses
  action: Scan transcript for Judge "GATE: <agent> — <reason>" messages; award 3 sparks each; cap at config.competition.gate_bonus.max_per_rt
- step: 30
  actor: amatelier.engine.scorer:compute_variance_flags
  action: Byzantine detector — flag agents whose scores deviate too far from peer consensus across recent RTs. Updates scores.is_flagged
- step: 31
  actor: amatelier.engine.roundtable_runner:_extract_and_register_ventures
  action: Parse <VENTURE>/<MOONSHOT>/<SCOUT> tags in transcript; register spark-stake contracts
- step: 32
  actor: amatelier.engine.roundtable_runner:_save_leaderboard
  action: Subprocess — scorer.py leaderboard; write benchmarks/leaderboard.json
- step: 33
  actor: amatelier.engine.roundtable_runner:_update_analytics
  action: Subprocess — analytics.py update; compute per-agent growth curves
- step: 34
  actor: amatelier.engine.roundtable_runner:_distill_skills
  action: Sonnet subprocess (claude -p with --no-session-persistence --dangerously-skip-permissions --max-budget-usd 5.00); produces 10-15 skill candidate objects as JSON; each has title, type (CAPTURE|FIX|DERIVE), agent, pattern, when_to_apply, structural_category, trigger_phase, primary_actor, problem_nature, agent_dynamic (DERIVE only), tags, one_liner
- step: 35
  actor: amatelier.engine.roundtable_runner:_append_novel_concepts
  action: Filter DERIVE skills; content-hash dedup; append to user_data_dir()/novel_concepts.json
- step: 36
  actor: amatelier.engine.therapist (subprocess, skipped if --skip-post)
  action: For each worker — run_session(rt_id, agent, digest, max_turns=2); 2-3 turn private interview. See separate therapist workflow
- step: 37
  actor: amatelier.engine.store:consume_boosts_after_rt
  action: Mark consumable purchase entries that fired this RT as consumed; write store/ledger.json
- step: 38
  actor: amatelier.engine.store:age_bulletin_requests
  action: Decrement age counters on open public-request entries; expire stale
- step: 39
  actor: amatelier.engine.agent_memory:age_goals
  action: Per worker — tick active goals forward; expire goals past their horizon
- step: 40
  actor: amatelier.engine.evolver:sync_skills_owned
  action: Per worker — refresh MEMORY.json.skills_owned from spark_ledger purchase entries
- step: 41
  actor: amatelier.engine.roundtable_runner:_notify_completion
  action: Write user_data_dir()/roundtable-server/latest-result.md; fire OS toast notification (best-effort)
- step: 42
  actor: amatelier.engine.roundtable_runner (__main__)
  action: If --summary: print format_digest_summary(digest). Else: print json.dumps(digest, indent=2, ensure_ascii=False)
```

Failure modes:

```yaml
- condition: Briefing file not found
  detection: step 5 raises FileNotFoundError
  recovery: runner exits non-zero; caller must fix path

- condition: Steward-registered files reference paths outside WORKSPACE_ROOT
  detection: step 6 warn-logs; steward marked inactive for this RT
  recovery: debate continues without steward; agents cannot ground claims in files

- condition: Entry fee deduction subprocess fails
  detection: step 13 try/except; logger.warning, continue
  recovery: RT proceeds without fee; manual ledger repair via scorer.py deduct-fee

- condition: Worker subprocess crashes
  detection: _check_and_restart polls proc.poll() at each round boundary; restarts with same model/tier
  recovery: Lost messages drop; restart captures new TURN signals. Repeated crashes → logged; RT continues with surviving workers

- condition: Gemini rate limit / API error
  detection: gemini_agent.py logs to user_data_dir()/roundtable-server/logs/gemini_errors.log
  recovery: step 25 scheduled refresh; or add --skip-naomi to bypass

- condition: SQLite busy / lock contention
  detection: db_cmd retries 3 times with 2s backoff; WAL mode + busy_timeout on connection
  recovery: Transient; persistent lock raises RuntimeError and kills RT

- condition: claude CLI missing in claude-code mode
  detection: llm_backend.ClaudeCLIBackend.available() returns False; resolve_mode falls through
  recovery: Auto-falls back to anthropic-sdk if ANTHROPIC_API_KEY set, else openai-compat, else BackendUnavailable at first LLM call

- condition: Judge Gate CONTINUE but max_rounds reached
  detection: Round loop exits without break
  recovery: convergence_reason stays None; digest reports converged=False

- condition: Keyboard interrupt
  detection: except KeyboardInterrupt in __main__ block
  recovery: db_cmd("cut", "keyboard interrupt") tries to close RT; exit 1
```

## Workflow: amatelier therapist (standalone or post-RT)

Trigger: `amatelier therapist --digest <path> [--agents CSV] [--turns N]` (or invoked automatically by roundtable_runner step 36 unless --skip-post).

```yaml
- step: 1
  actor: amatelier.engine.therapist (__main__)
  action: argparse → digest path required; optional agents CSV (default: all workers in digest); default turns=2
- step: 2
  actor: amatelier.engine.therapist:run_therapist
  action: Load digest JSON; iterate agents list
- step: 3
  actor: amatelier.engine.therapist:run_session (per agent)
  action: _load_therapist_context() — assemble therapist persona, case_notes, frameworks (GROW+AAR, SBI, OARS)
- step: 4
  actor: amatelier.engine.therapist:_load_agent_state
  action: Read MEMORY.md, MEMORY.json, behaviors.json, metrics.json, sessions/ for this agent
- step: 5
  actor: amatelier.engine.therapist:_extract_agent_data
  action: Pull this agent's contributions, scores, budget_usage, stability from digest
- step: 6
  actor: amatelier.engine.therapist:_compute_skill_impact
  action: Correlate owned skills with this RT's scores; flag fading or unused skills
- step: 7
  actor: amatelier.engine.therapist:_build_agent_brief
  action: Render combined context block for the agent-side of the interview
- step: 8
  actor: amatelier.engine.therapist:_call_therapist
  action: LLM call — therapist opens the interview (opus or configured model). Uses opening_data from step 5-6 on first turn
- step: 9
  actor: amatelier.engine.therapist:_call_agent
  action: LLM call — agent responds (own model/tier). Loops for max_turns exchanges
- step: 10
  actor: amatelier.engine.therapist:_parse_outcomes
  action: Scan conversation for structured blocks — behavioral_deltas, memory_updates, session_summary, trait adjustments, store_request blocks (JSON fenced)
- step: 11
  actor: amatelier.engine.therapist:_apply_outcomes
  action: Dispatch parsed outcomes:
    - behaviors.json — via evolver.apply_behavior_delta
    - MEMORY.md / MEMORY.json — append update blocks
    - sessions/<rt_id>.md — write interview summary
    - store requests — _process_store_request (attempt_purchase if affordable, else queue)
    - goals — agent_memory.add_goal
- step: 12
  actor: amatelier.engine.therapist:_update_case_notes
  action: Increment sessions_conducted; append to clinical_observations, active_hypotheses, intervention_history
- step: 13
  actor: amatelier.engine.therapist:_save_case_notes
  action: Write user_data_dir()/agents/therapist/case_notes/<name>.json
- step: 14
  actor: amatelier.engine.therapist:_mark_private_requests_addressed
  action: Update status of any private-request entries in store/ledger.json that this session addressed
- step: 15
  actor: amatelier.engine.therapist:_generate_report (post-all-agents)
  action: Aggregate per-agent summaries into user_data_dir()/roundtable-server/therapist-report-<rt_id>.md
```

Failure modes:

```yaml
- condition: Digest JSON malformed
  detection: json.load raises
  recovery: Exit non-zero with path in error

- condition: Agent state files missing
  detection: step 4 — paths.user_agent_dir(name) empty
  recovery: ensure_user_data() re-seeds from bundled (first-run path); subsequent failures abort this agent only

- condition: LLM timeout during interview
  detection: _call_llm timeout raises
  recovery: Session marked incomplete; outcomes partially applied; no case notes increment

- condition: Parsed outcomes include purchase for insufficient sparks
  detection: _process_store_request — attempt_purchase returns insufficient_funds
  recovery: Request queued to bulletin board instead; logged
```

## Workflow: steward research request (mid-debate)

Trigger: worker emits `[[request: <natural language query>]]` in a speak/rebuttal/floor turn.

```yaml
- step: 1
  actor: amatelier.engine.roundtable_runner:run_roundtable (post-speaker scan)
  action: After _call_speaker returns, scan last_msg_text with steward_dispatch.parse_requests; regex extracts [[request:...]] payload
- step: 2
  actor: amatelier.engine.steward_dispatch:StewardBudget.spend
  action: Decrement remaining count for this agent; if 0, request denied (no budget)
- step: 3
  actor: amatelier.engine.steward_dispatch:StewardTask (constructor)
  action: Spawn background thread; task.done = threading.Event
- step: 4
  actor: amatelier.engine.roundtable_runner:run_roundtable
  action: Append agent to back of speak_queue (deferred); record in pending_steward dict
- step: 5
  actor: amatelier.engine.steward_dispatch:StewardTask._run (thread)
  action: Try deterministic path first — JSON filter, grep, extract. If successful, skip subagent
- step: 6
  actor: amatelier.engine.steward_dispatch (subagent fallback)
  action: subprocess — claude -p with --allowedTools Read,Grep,Glob --max-budget-usd from config.steward. Registered files appear in prompt context. Model = haiku_model (default) or sonnet_model for complex queries
- step: 7
  actor: amatelier.engine.steward_dispatch:StewardTask._run (completion)
  action: task.done.set(); result stored on task with status, elapsed_s, output
- step: 8
  actor: amatelier.engine.roundtable_runner:run_roundtable
  action: Before next speaker or at INTERMISSION, poll pending_steward tasks; if done.is_set(), call format_result(agent, request, result) and db_cmd("speak", "runner", inject_msg)
- step: 9
  actor: amatelier.engine.steward_dispatch:StewardLog.record
  action: Append to in-memory log (agent, request, result, round_num); exposed in digest for audit
- step: 10
  actor: amatelier.engine.roundtable_runner:run_roundtable
  action: Before REBUTTAL phase starts, drain all remaining pending_steward; inject all results
- step: 11
  actor: deferred agent (on next turn)
  action: When agent is popped from back of queue, they see injected [Research result for <agent>] message in their listen context; can cite in their speak
```

Failure modes:

```yaml
- condition: Subagent timeout (default 120s)
  detection: StewardTask.wait(timeout=timeout_seconds+10) returns with status=timeout
  recovery: Inject "timeout" error message; agent speaks without citation

- condition: Registered file missing on disk
  detection: Subagent Read tool returns error
  recovery: Error captured in result; injected to chat; agent cited anyway or speaks without

- condition: Request outside registered files
  detection: Subagent prompt restricts to listed paths; refuses out-of-scope reads
  recovery: Agent receives "out of scope" result; may retry with different phrasing if budget allows

- condition: All workers deferred simultaneously
  detection: remaining_speakers empty but pending_steward non-empty
  recovery: INTERMISSION — runner blocks on task.wait for each; resumes SPEAK phase after
```

## Workflow: skill distillation (post-RT)

Trigger: end of round loop inside run_roundtable; always runs (even with --skip-post).

```yaml
- step: 1
  actor: amatelier.engine.roundtable_runner:_distill_skills
  action: Build prompt — transcript (capped 50K chars), RT metadata, required JSON schema with 12 fields per skill
- step: 2
  actor: subprocess claude -p (sonnet)
  action: flags — --no-session-persistence --output-format text --disable-slash-commands --dangerously-skip-permissions --max-budget-usd 5.00; timeout 180s; cwd=WORKSPACE_ROOT
- step: 3
  actor: amatelier.engine.roundtable_runner:_distill_skills (parse)
  action: Locate first '[' and last ']' in stdout; json.loads; validate array
- step: 4
  actor: amatelier.engine.roundtable_runner:_append_novel_concepts (DERIVE only)
  action: Filter type=="DERIVE"; compute content hash of title+pattern; dedup against existing novel_concepts.json
- step: 5
  actor: amatelier.engine.distiller:create_skill_entry (via runner or backfill)
  action: For each distilled skill — generate skill_id; attach rt_id, timestamp, originator; prepare entry dict
- step: 6
  actor: amatelier.engine.distiller:_judge_gate
  action: Score extraction worthiness (redundancy, specificity); entries failing the gate are flagged but still included in index with lower priority
- step: 7
  actor: amatelier.engine.distiller:save_index / save_skill_to_agent
  action: Append to user_data_dir()/shared-skills/index.json; write per-agent pending skill markdown
- step: 8
  actor: opus-admin (manual curation step, out-of-band)
  action: Admin reviews candidates; calls distiller.promote_to_shared for the best 3-5; promoted entries move to shared-skills/entries/ and become store-purchasable
```

Failure modes:

```yaml
- condition: Sonnet returns non-JSON output
  detection: json_start/json_end markers missing
  recovery: Log "non-json" error; return empty skills list; digest has no new entries

- condition: Subprocess timeout (180s)
  detection: subprocess.TimeoutExpired
  recovery: Log "timeout" error; return empty skills list

- condition: Subprocess exit non-zero
  detection: result.returncode != 0
  recovery: Log exit code; return empty skills list
```

## Workflow: amatelier refresh-seeds (user opt-in)

Trigger: `amatelier refresh-seeds [--agent NAME] [--force] [--dry-run]`.

```yaml
- step: 1
  actor: amatelier.cli:_run_refresh_seeds
  action: argparse flags; resolve bundled_agents = paths.bundled_assets_dir() / "agents"
- step: 2
  actor: amatelier.cli:_run_refresh_seeds
  action: If --agent given — single-agent list; else — sorted(bundled_agents.iterdir())
- step: 3
  actor: amatelier.cli:_run_refresh_seeds (per agent)
  action: For CLAUDE.md and IDENTITY.md:
    - Read bundled content
    - Compare to user_agent_dir()/<file>
    - If identical — skip (already current)
    - If differs and not --force — skip (user-modified, preserved)
    - If differs and --force or no user copy — write (unless --dry-run)
- step: 4
  actor: amatelier.cli:_run_refresh_seeds
  action: Print Refreshed/Skipped/WRITE counts; footnote reminding that MEMORY/behaviors/metrics are untouched
```

Failure modes:

```yaml
- condition: Bundled agents directory missing (corrupt install)
  detection: step 1 — not bundled_agents.exists()
  recovery: Print error, exit 1; user reinstalls
```

## Workflow: first-run bootstrap

Trigger: first `import amatelier` or first `amatelier <any-command>` on a machine where user_data_dir() has no .bootstrap-complete sentinel.

```yaml
- step: 1
  actor: amatelier/__init__.py (module import)
  action: sys.path prepends src/amatelier/engine and src/amatelier/store for flat-import compat; try: paths.ensure_user_data()
- step: 2
  actor: amatelier.paths:ensure_user_data
  action: Resolve user_data_dir (platformdirs or AMATELIER_WORKSPACE); check for .bootstrap-complete sentinel — if present and not force, return immediately
- step: 3
  actor: amatelier.paths:ensure_user_data
  action: mkdir -p the writable tree — roundtable-server/, roundtable-server/logs/, agents/, store/, shared-skills/
- step: 4
  actor: amatelier.paths:_load_config_for_bootstrap
  action: Read user config override if present, else bundled config; fall back to empty dict on error
- step: 5
  actor: amatelier.paths:ensure_user_data
  action: Union of team.workers keys, admin/judge/therapist names, and subdirs of bundled agents/ — call _copy_agent_seed per name
- step: 6
  actor: amatelier.paths:_copy_agent_seed (per agent)
  action: mkdir user_agent_dir/<name>; copy CLAUDE.md and IDENTITY.md from bundled (shutil.copy2, skip if target exists); mkdir sessions/ and skills/; initialize empty MEMORY.md, MEMORY.json="{}", metrics.json="{}", behaviors.json="{}"
- step: 7
  actor: amatelier.paths:ensure_user_data
  action: _init_json(store/ledger.json, {}); _init_json(novel_concepts.json, []); _init_json(shared-skills/index.json, {"entries": []})
- step: 8
  actor: amatelier.paths:ensure_user_data
  action: Write .bootstrap-complete sentinel with content "1"
```

Failure modes:

```yaml
- condition: AMATELIER_WORKSPACE points at unwritable path
  detection: mkdir or write raises PermissionError
  recovery: __init__.py swallows at import time; first real write (e.g. open RT) surfaces the concrete error

- condition: platformdirs cannot determine user data dir
  detection: unlikely; returns empty string → Path("") which is invalid
  recovery: Same as above — deferred; set AMATELIER_WORKSPACE explicitly

- condition: Bundled agents subdirectory missing (corrupt install)
  detection: bundled_agents.exists() False in step 5
  recovery: Loop skipped; empty user-data tree; refresh-seeds also fails with same symptom
```

================================================================================
FILE: llm/EXAMPLES.md
================================================================================

# Examples

> Tested, copy-runnable examples. Code blocks are extracted and executed by CI (checks/snippets.py). Broken example = red build.

## Conventions

- Each example is self-contained
- Prereqs listed at top of each example
- Expected output shown in a second `text` block
- Language tags on code blocks so extractor routes correctly
- Shell examples use POSIX syntax; Windows users run in bash/WSL or translate env var syntax

## Example: hello world

Prereqs: `pip install amatelier`

```python
from amatelier import hello

print(hello("world"))
```

Expected output:

```text
Hello, world!
```

## Example: anthropic-sdk backend, minimal roundtable

Prereqs: `pip install amatelier` — ANTHROPIC_API_KEY and GEMINI_API_KEY in environment.

```bash
export ANTHROPIC_API_KEY="sk-ant-..."
export GEMINI_API_KEY="..."
export AMATELIER_MODE="anthropic-sdk"

amatelier config

amatelier roundtable \
  --topic "what is the most undervalued idea in open-source software?" \
  --briefing examples/briefings/hello-world.md \
  --max-rounds 1 \
  --budget 1 \
  --summary
```

Expected output:

```text
amatelier 0.3.0

LLM backend
  active mode: anthropic-sdk

...

TOPIC: what is the most undervalued idea in open-source software?
ROUNDS: 1
MESSAGES: 12
CONVERGED: ...
JUDGE INTERVENTIONS: ...
BUDGET USAGE:
  elena: spent 0/1
  marcus: spent 0/1
  clare: spent 0/1
  simon: spent 0/1
  naomi: spent 0/1
...
```

Digest lands at `$(amatelier config | grep user_data_dir)/roundtable-server/digest-<rt_id>.json`.

## Example: openrouter backend with custom model map

Prereqs: `pip install amatelier` — OPENROUTER_API_KEY in environment. Custom model map pinning tiers to specific OpenRouter model IDs.

```bash
export OPENROUTER_API_KEY="sk-or-..."

mkdir -p "$(amatelier config --json | python -c 'import sys,json; print(json.load(sys.stdin)["paths"]["user_data_dir"])')"

cat > "$(amatelier config --json | python -c 'import sys,json; print(json.load(sys.stdin)["paths"]["user_data_dir"])')/config.json" << 'EOF'
{
  "llm": {
    "mode": "openai-compat",
    "openai_compat": {
      "base_url": "https://openrouter.ai/api/v1",
      "api_key_env": "OPENROUTER_API_KEY",
      "model_map": {
        "sonnet": "anthropic/claude-sonnet-4",
        "haiku": "anthropic/claude-haiku-4-5",
        "opus": "anthropic/claude-opus-4"
      }
    }
  }
}
EOF

amatelier config --json

amatelier roundtable \
  --topic "pros and cons of monorepos" \
  --briefing examples/briefings/hello-world.md \
  --max-rounds 1 \
  --budget 1 \
  --summary
```

Expected snippet of `amatelier config --json`:

```text
{
  "version": "0.3.0",
  "llm": {
    ...
    "active_mode": "openai-compat",
    "explicit_override": null
  },
  ...
}
```

## Example: local Ollama backend

Prereqs: Ollama installed and running (https://ollama.ai). Pull a model locally first.

```bash
ollama pull llama3.1:70b
ollama pull llama3.1:8b

export AMATELIER_LLM_API_KEY="ollama"
export AMATELIER_MODE="openai-compat"

AMATELIER_USERDATA="$(amatelier config --json | python -c 'import sys,json; print(json.load(sys.stdin)["paths"]["user_data_dir"])')"

cat > "$AMATELIER_USERDATA/config.json" << 'EOF'
{
  "llm": {
    "mode": "openai-compat",
    "openai_compat": {
      "base_url": "http://localhost:11434/v1",
      "api_key_env": "AMATELIER_LLM_API_KEY",
      "model_map": {
        "sonnet": "llama3.1:70b",
        "haiku": "llama3.1:8b",
        "opus": "llama3.1:70b"
      }
    }
  }
}
EOF

amatelier config
amatelier roundtable \
  --topic "what makes a good README" \
  --briefing examples/briefings/hello-world.md \
  --max-rounds 1 \
  --budget 1 \
  --skip-naomi \
  --summary
```

Expected output line confirms openai-compat mode pointing at localhost.

```text
  active mode: openai-compat
  ...
  user_data_dir        ...
```

## Example: custom briefing file

Prereqs: any backend configured. Create a briefing then invoke against it by path.

```bash
cat > /tmp/refactor-briefing.md << 'EOF'
# Briefing: monolith split

## Objective

Decide whether splitting service X from the monolith is worth the coordination overhead.

## Context

- Team size: 8 engineers
- Service X: 4k lines, 3 endpoints, shared DB with monolith
- Deploy cadence: monolith deploys 5x/day, X changes 2x/week

## Constraints

- No downtime migration
- Budget: one quarter

## Success criteria

Each worker proposes a split-or-stay position with at least one load-bearing tradeoff cited from the context above.

## Steward-Registered Files

(none — this briefing uses no external file references)
EOF

amatelier roundtable \
  --topic "split service X from monolith, yes or no" \
  --briefing /tmp/refactor-briefing.md \
  --budget 3 \
  --summary
```

Expected output: digest with 5 workers posting positions; Judge scores; skills distilled.

## Example: skip Naomi, single-worker smoke test

Prereqs: any Claude-compatible backend (claude-code mode or anthropic-sdk mode). No Gemini key required with `--skip-naomi`.

```bash
amatelier roundtable \
  --topic "hello" \
  --briefing examples/briefings/hello-world.md \
  --workers elena \
  --max-rounds 1 \
  --budget 1 \
  --skip-naomi \
  --skip-post \
  --summary
```

Expected output:

```text
...
TOPIC: hello
ROUNDS: 1
MESSAGES: ...
CONTRIBUTIONS:
  elena: ...
  judge: ...
FINAL POSITIONS:
  [elena]: ...
```

`--skip-post` halts after distillation — no therapist run, no store cleanup, no leaderboard update.

## Example: reading the digest in Python

Prereqs: at least one completed roundtable. Locate digest via `amatelier config`.

```python
import json
from pathlib import Path
from amatelier import paths

digest_dir = paths.user_digest_dir()
digests = sorted(digest_dir.glob("digest-*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
if not digests:
    raise SystemExit("no digests yet — run: amatelier roundtable --topic ... --briefing ...")

latest = digests[0]
d = json.loads(latest.read_text(encoding="utf-8"))

print(f"rt_id: {d.get('rt_id')}")
print(f"topic: {d.get('topic')}")
print(f"rounds: {d.get('rounds')}")
print(f"converged: {d.get('converged')}")
print(f"contributions: {d.get('contributions')}")
for agent, pos in d.get("final_positions", {}).items():
    print(f"  [{agent}] {pos[:120]}...")
```

Expected output (shape):

```text
rt_id: <hex>
topic: <your topic>
rounds: <int>
converged: True|False
contributions: {'elena': <int>, 'marcus': <int>, ...}
  [elena] ...
  [marcus] ...
  ...
```

## Example: refresh agent seeds after a package upgrade

Prereqs: `pip install --upgrade amatelier`. By default, pip upgrades do NOT touch user-edited persona files in `user_data_dir()/agents/<name>/`. Run `refresh-seeds` to pull shipped updates.

```bash
amatelier refresh-seeds --dry-run

amatelier refresh-seeds --force
```

Expected output of `--dry-run`:

```text
dry run — no files written

Refreshed: 0

Skipped: <N>
  [SKIP] elena/CLAUDE.md (already current)
  [SKIP] marcus/CLAUDE.md (user-modified; use --force to overwrite)
  ...
```

Output of `--force`:

```text
Refreshed: <N>
  [WRITE] marcus/CLAUDE.md
  ...

Note: <M> agent(s) refreshed. Their accumulated MEMORY.md / behaviors.json / metrics.json are untouched — only the persona rules and identity seeds were overwritten.
```

Single-agent form: `amatelier refresh-seeds --agent elena --force`.

## Example: programmatic LLM backend

Prereqs: `pip install amatelier` and one of the supported backends configured. Useful when a calling program needs the backend abstraction but not the full roundtable orchestration.

```python
from amatelier.llm_backend import get_backend, describe_environment

print(describe_environment())

backend = get_backend()

result = backend.complete(
    system="You are a terse code reviewer.",
    prompt="Review: def add(a, b): return a - b",
    model="haiku",
    max_tokens=200,
    timeout=60.0,
)

print(f"backend: {result.backend}")
print(f"model:   {result.model}")
print(f"latency: {result.latency_ms:.0f} ms")
print(f"tokens:  in={result.input_tokens} out={result.output_tokens}")
print(f"text:    {result.text[:400]}")
```

Expected output (shape):

```text
{'claude-code': {'available': ..., 'detected_via': ...}, 'anthropic-sdk': {...}, ...}
backend: anthropic-sdk
model:   claude-haiku-4-5-20251001
latency: <int> ms
tokens:  in=<int> out=<int>
text:    The function subtracts instead of adding...
```

## Example: tail a live roundtable

Prereqs: a roundtable in progress (start one in another terminal with `amatelier roundtable ...`).

```bash
amatelier watch
```

Expected output: streams new chat messages as they land in `user_data_dir()/roundtable-server/roundtable.db`. Zero LLM cost — pure SQLite reads. Shows speaker name, message preview, and Judge interventions in real time. Exit with Ctrl-C.

