diff --git a/.env.example b/.env.example index fb3eff7..41b1d6d 100644 --- a/.env.example +++ b/.env.example @@ -25,28 +25,55 @@ SMRITI_DB_MODE=local # SMRITI_DB_MODE=postgres # DATABASE_URL=postgresql://smriti:smriti@localhost:5432/smriti -# Provider API keys — uncomment and set the ones you want to use. -# If unset, the backend falls back to config/providers.yaml, then to -# the mock provider (deterministic responses, no real LLM calls). +# ──────────────────────────────────────────────────────────────────────── +# Provider configuration (LLM-backed features only) +# ──────────────────────────────────────────────────────────────────────── +# +# Core Smriti — setup, doctor, quickstart, state/current/metrics, claims, +# attach, and hand-written checkpoints — works WITHOUT any API key. +# +# These features require a real configured provider: +# - `smriti checkpoint create --extract` +# - `smriti checkpoint review` +# - checkpoint draft +# - chat UI send loop +# +# Without a provider, those paths refuse to run (HTTP 412) rather than +# silently returning placeholder MockAdapter content. Mock content is for +# tests and demos only; it must never be committed into a real project. +# +# Uncomment ONE of the keys below for a hosted provider, OR configure the +# generic provider (further down) for a local OpenAI-compatible model. # # IMPORTANT: do not leave a key set to empty (e.g. OPENAI_API_KEY=). # An empty value is actively placed into os.environ by dotenv and can -# mask the yaml fallback in some configurations. Either set a real -# key or leave the line commented out. +# mask the yaml fallback. Either set a real key or leave the line commented. +# Setting OPENAI_MODEL alone (below) without OPENAI_API_KEY is NOT enough — +# the model needs an actual provider with credentials behind it. +# # OPENAI_API_KEY=your-openai-key-here # ANTHROPIC_API_KEY=your-anthropic-key-here # OPENROUTER_API_KEY=your-openrouter-key-here -# Default model for background intelligence (extraction, draft, review) +# Default model for background intelligence (extraction, draft, review). +# Only takes effect when a provider key (above) is set or the generic +# provider (below) is configured. OPENAI_MODEL=gpt-4o-mini -# Generic OpenAI-compatible provider (Ollama, LM Studio, vLLM, Together, etc.) -# Set the API URL to use a local or cheap model instead of OpenAI/Anthropic. -# API key is optional — local servers like Ollama don't require one. -# Then set background_intelligence.provider to "generic" in providers.yaml. +# Generic OpenAI-compatible provider — Ollama, LM Studio, vLLM, Together, etc. +# Use this slot for a LOCAL model (or any OpenAI-API-compatible endpoint). +# API key may be optional for local servers; OpenAI SDK accepts any non-empty +# string. Then set background_intelligence.provider to "generic" in +# backend/config/providers.yaml. +# +# Extraction is usually a lightweight structured-output task and is often +# handled well by cost-efficient or local models, but quality varies — try +# `smriti checkpoint create --extract --dry-run` on a representative +# document before relying on a small model for real work. +# # SMRITI_GENERIC_API_URL=http://localhost:11434/v1 # SMRITI_GENERIC_MODEL=llama3.1:8b -# SMRITI_GENERIC_API_KEY= +# SMRITI_GENERIC_API_KEY=not-required # App DEBUG=false diff --git a/AGENTS.md b/AGENTS.md index 8eacc19..ed2bfa5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -582,6 +582,40 @@ know about. Concretely: - **You are about to explore an alternative direction and want to preserve the current line.** Fork first, then checkpoint on the fork. +### 4.1 Before your first `--extract`: verify the provider + +The extract path uses Smriti's background LLM to turn freeform markdown +into structured checkpoint fields. It **requires a real configured +provider** (OpenAI / Anthropic / OpenRouter / generic OpenAI-compatible +endpoint). If no provider is configured, the extract endpoint returns +**HTTP 412 Precondition Failed** rather than silently falling back to +mock content — mock content committed into a real Space pollutes the +reasoning state Smriti exists to keep trustworthy. + +Run `smriti doctor` once at the start of a session that intends to use +`--extract`, and read the `background provider:` line. + +``` +smriti doctor +``` + +Look for: + +- **`background provider: ready (real LLM extraction enabled)`** — + good, use `--extract` freely. +- **`background provider: ⚠ MOCK or DISABLED — ... will fail until a + provider is configured`** — do **not** use `--extract`. Either ask + the human to configure a provider (OpenAI/Anthropic/OpenRouter key, + or a local OpenAI-compatible endpoint via the generic provider), or + write the checkpoint **manually as JSON** (see 4.1 below). Do not + attempt to commit mock content as if it were real reasoning state. + +For scripted/CI use, `smriti doctor --strict` exits non-zero +(EX_CONFIG / 78) when the background provider is mock/disabled — +useful as a pre-flight gate before any `--extract` step. + +### 4.2 The extract path (provider required) + Use the **extract path**, not hand-written JSON. Pass freeform markdown describing the inflection point and Smriti's background LLM will pull out the structured fields (message, objective, summary, decisions, @@ -611,7 +645,7 @@ Always tag `author_agent` with a stable identifier for your agent agents know who wrote what on the shared timeline. Inconsistent or missing `author_agent` makes divergence unattributable. -### 4.1 Checkpoint notes: annotating without checkpointing +### 4.3 Checkpoint notes: annotating without checkpointing Sometimes you need to add context to an existing checkpoint without creating a new one. Checkpoint notes are additive annotations — diff --git a/README.md b/README.md index da4df53..1a4b40c 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Backend reachable, CLI/backend versions aligned, provider status. If anything's smriti quickstart ``` -Seeds a `smriti-demo` Space — one finished mini-project (a rate-limiting feature built by two agents, with a branch explored and dropped) — and prints a ~3-minute guided walkthrough. Works without API keys (mock-mode extraction). Clean up with `smriti quickstart --remove`. +Seeds a `smriti-demo` Space — one finished mini-project (a rate-limiting feature built by two agents, with a branch explored and dropped) — and prints a ~3-minute guided walkthrough. Works without API keys — quickstart seeds pre-built checkpoints, no live extraction. (For live `--extract` you need a real provider; see [Provider configuration](#provider-configuration-llm-backed-features).) Clean up with `smriti quickstart --remove`. ### 4. Attach your own project — `smriti init` @@ -211,27 +211,56 @@ The primitives that turn "shared state" from a phrase into something that actual --- -## API keys (optional) +## Provider configuration (LLM-backed features) -**The core coordination loop runs without API keys.** Setup, `smriti doctor`, `smriti quickstart`, `smriti state` / `current` / `metrics`, claims, attachments, repo-state drift detection, hand-written checkpoints, and the chat UI's read-only dashboards (timeline, checkpoints, claims, drift signals) all work with no key. +Smriti draws a hard line between **core coordination** (works with no API key) and **LLM-backed features** (require a configured provider). -API keys are only needed for the LLM-assisted features: +### Works with no key -- `smriti checkpoint create --extract` — extract structured fields from freeform markdown -- Checkpoint draft and consistency review (`smriti checkpoint review`) -- The chat UI's send loop — where the agent actually responds to your messages +- `setup` / `doctor` / `quickstart` +- `smriti state` / `current` / `metrics` +- claims (`smriti claim`, claim listing) +- attach / no-arg project workflow +- manual structured checkpoints (`smriti checkpoint create ` with a JSON payload on stdin) +- repo-state drift detection +- the chat UI's read-only dashboards (timeline, checkpoints, claims, drift signals) -Smriti supports OpenAI, Anthropic, OpenRouter, and any OpenAI-compatible provider (Ollama, LM Studio, vLLM) via the generic provider slot. Set keys in `.env` when you want those features for real: +### Require a real provider + +- `smriti checkpoint create --extract` — extracts structured fields from freeform markdown +- `smriti checkpoint review` — consistency review of a checkpoint +- checkpoint draft +- the chat UI's send loop — where the agent actually responds + +Without a configured provider, these refuse to run rather than silently return placeholder content. `--extract` returns **HTTP 412 Precondition Failed** with an actionable error listing the configuration paths. **Mock content is never silently committed into a real project** — that would pollute reasoning state, which Smriti exists to keep trustworthy. + +### Configure a provider + +Set one of these in `.env` (or your shell): ``` OPENAI_API_KEY=... ANTHROPIC_API_KEY=... OPENROUTER_API_KEY=... -SMRITI_GENERIC_API_URL=http://localhost:11434/v1 # for Ollama / LM Studio / vLLM -SMRITI_GENERIC_MODEL=llama3.1:8b ``` -Without a key set, the LLM-assisted paths return deterministic placeholder content so the mechanics still work — handy for trying the coordination loop end-to-end. Add a key when you want real extraction. +Or, for a **local OpenAI-compatible model** (Ollama, LM Studio, vLLM, Together, etc.): + +``` +SMRITI_GENERIC_API_URL=http://localhost:11434/v1 # your local server +SMRITI_GENERIC_MODEL=llama3.1:8b +# SMRITI_GENERIC_API_KEY=not-required # most local servers don't need one +``` + +Then set `background_intelligence.provider: generic` in `backend/config/providers.yaml`. + +Extraction is a relatively lightweight structured-output task and is usually handled well by cost-efficient or local OpenAI-compatible models. That said, *quality varies by model* — validate with `smriti checkpoint create --extract --dry-run` on a representative document before relying on a small model for real work. + +After configuring, run `smriti doctor` (optionally with `--strict`) to confirm the background provider line reads `ready`. `--strict` exits non-zero (EX_CONFIG / 78) if the provider is `mock_or_disabled`, which makes it safe to use in CI before any `--extract` step. + +### Mock mode + +A deterministic `MockAdapter` exists for tests, demos, and the `quickstart` mechanics. It returns a fixed JSON blob (containing literal strings like `"Mock decision from provider"`). **It is never the default for `--extract`** — it only runs when the caller explicitly opts in (e.g. tests passing `use_mock=true` to the HTTP endpoint). Do not commit mock-extracted content into a real project Space. --- diff --git a/backend/app/api/routes/checkpoint.py b/backend/app/api/routes/checkpoint.py index b35a6bb..57cb7ff 100644 --- a/backend/app/api/routes/checkpoint.py +++ b/backend/app/api/routes/checkpoint.py @@ -20,7 +20,7 @@ from app.schemas import ( ReviewIssue, ) from app.providers.registry import get_adapter, get_mock_adapter -from app.config_loader import get_config +from app.config_loader import get_config, ProviderNotConfiguredError router = APIRouter() logger = logging.getLogger(__name__) @@ -403,23 +403,57 @@ Rules: - Output ONLY valid JSON. No markdown wrappers, no explanation. """ - try: - if request.use_mock: - adapter = get_mock_adapter() - bg_model = "mock" - else: - cfg = get_config() - bg_model = cfg.background.model - # allow_mock=True so an unconfigured test env falls back to - # MockAdapter (which supports JSON-mode responses); production - # envs always have a real provider configured and go through - # the real adapter path. - adapter = get_adapter(cfg.background.provider, allow_mock=True) - except Exception as e: - raise HTTPException( - status_code=500, - detail=f"Background provider not configured. Error: {e}", - ) + # Resolve the adapter. + # + # Contract (post launch-blocker fix): + # - use_mock=True (explicit opt-in): always return MockAdapter. Used by + # tests and demos that need deterministic JSON without a real provider. + # - use_mock=False (default): require a real configured provider. If none + # is configured, return 412 Precondition Failed with a structured + # detail explaining how to fix it. Never silently fall back to mock — + # mock content committed into a real project pollutes reasoning state. + if request.use_mock: + adapter = get_mock_adapter() + bg_provider = "mock" + bg_model = "mock" + else: + cfg = get_config() + bg_provider = cfg.background.provider + bg_model = cfg.background.model + try: + adapter = get_adapter(bg_provider, allow_mock=False) + except ProviderNotConfiguredError as e: + logger.warning( + "extract refused: background provider '%s' not configured (%s)", + bg_provider, e, + ) + raise HTTPException( + status_code=412, + detail={ + "error": "provider_not_configured", + "message": ( + f"Background LLM provider '{bg_provider}' is not configured. " + "Real checkpoint extraction requires a real provider — refusing " + "to silently fall back to mock content." + ), + "provider": bg_provider, + "model": bg_model, + "fix": [ + "Set OPENAI_API_KEY in .env (or your shell) for OpenAI.", + "Set ANTHROPIC_API_KEY for Anthropic.", + "Set OPENROUTER_API_KEY for OpenRouter.", + "For a local OpenAI-compatible model (Ollama, LM Studio, vLLM): " + "set SMRITI_GENERIC_API_URL + SMRITI_GENERIC_MODEL, then set " + "background_intelligence.provider to 'generic' in " + "backend/config/providers.yaml.", + "Or skip extraction and create a checkpoint manually with " + "`smriti checkpoint create ` and a JSON payload on stdin.", + "For tests/demos only: pass use_mock=true to explicitly request " + "the MockAdapter (do NOT commit that output into a real project).", + ], + "details": str(e), + }, + ) try: raw_response = adapter.send( @@ -441,6 +475,8 @@ Rules: open_questions=_dedup(data.get("open_questions", [])), entities=_dedup(data.get("entities", [])), artifacts=[a for a in data.get("artifacts", []) if isinstance(a, dict)], + provider=bg_provider, + model=bg_model, ) except json.JSONDecodeError: logger.error(f"Extract LLM returned invalid JSON: {raw_response}") diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index f34d778..a89a277 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -158,3 +158,9 @@ class CheckpointExtractResponse(BaseModel): open_questions: list[str] = Field(default_factory=list) entities: list[str] = Field(default_factory=list) artifacts: list[dict] = Field(default_factory=list) + # Echo the provider/model that actually answered, so callers can confirm + # whether the extraction came from a real LLM or the mock adapter. The + # CLI surfaces this on every --extract commit and uses it to refuse to + # persist mock content into a real project. + provider: str = Field("", description="Provider that produced this extraction (e.g. 'openai', 'anthropic', 'mock').") + model: str = Field("", description="Model identifier that produced this extraction (or 'mock').") diff --git a/backend/tests/integration/test_checkpoint_extract.py b/backend/tests/integration/test_checkpoint_extract.py index 3cc64f8..70d2eb8 100644 --- a/backend/tests/integration/test_checkpoint_extract.py +++ b/backend/tests/integration/test_checkpoint_extract.py @@ -1,11 +1,27 @@ """Integration tests for POST /api/v5/checkpoint/extract. -Tests use MockAdapter's JSON-mode path (registry._MOCK_JSON_RESPONSE), -which returns a deterministic canned blob when the caller passes -response_format={"type": "json_object"}. The extract endpoint always -requests JSON mode, so every test hits the canned response. +Post launch-blocker fix (provider-extract-safety): + + - The DEFAULT path (no `use_mock` flag) requires a real configured + background provider. If none is configured, the endpoint returns + HTTP 412 with a structured `provider_not_configured` detail. It + never silently falls back to MockAdapter — that would pollute a + real user's reasoning state with placeholder content. + + - The EXPLICIT mock path (`use_mock=True`) still works for tests + and demos. It always returns the canned MockAdapter response. + +These tests pin both contracts. """ +import json +import pytest + +from app.config_loader import ProviderNotConfiguredError +from app.providers.base import ProviderAdapter +from app.providers import registry as provider_registry +from app.api.routes import checkpoint as checkpoint_route + def _sample_markdown() -> str: return """# Design: envdiff CLI @@ -27,8 +43,11 @@ def main(): """ +# ── Explicit mock path: use_mock=True ───────────────────────────────────────── + + def test_extract_happy_path_with_mock(client): - """Extract endpoint returns canned mock fields when use_mock=True.""" + """use_mock=True: MockAdapter returns canned content, 200 OK.""" r = client.post( "/api/v5/checkpoint/extract", json={"content": _sample_markdown(), "use_mock": True}, @@ -51,36 +70,99 @@ def test_extract_happy_path_with_mock(client): assert "MockEntity" in data["entities"] assert len(data["artifacts"]) == 1 assert data["artifacts"][0]["label"] == "Mock artifact" + # Echo fields must mark this as mock so callers can refuse to commit. + assert data["provider"] == "mock" + assert data["model"] == "mock" -def test_extract_default_path_returns_shape(client): - """Default path (no use_mock flag) returns a valid CheckpointExtractResponse - shape regardless of which provider answers. In a test env with no API - keys this hits MockAdapter via the allow_mock=True fallback; in a dev - env with real keys it hits the real provider and returns real extracted - fields. Either way the response must be well-shaped. +# ── Default path without provider: must fail loud (the launch-blocker fix) ─── + + +def test_extract_without_provider_fails_loud(client, monkeypatch): + """Default path (no use_mock) + no real provider configured: HTTP 412 with + a structured 'provider_not_configured' detail. Never silent mock. + + This is the regression test for the launch-blocker bug where the extract + endpoint silently returned MockAdapter content like + "Mock decision from provider" when no provider was configured. """ + # Force the provider-not-configured state regardless of test-env API keys. + def _raise_not_configured(provider: str, allow_mock: bool = False): + # The endpoint must call with allow_mock=False (no silent fallback). + assert allow_mock is False, ( + "extract endpoint must call get_adapter with allow_mock=False" + ) + raise ProviderNotConfiguredError(f"no API key for {provider}") + + monkeypatch.setattr(checkpoint_route, "get_adapter", _raise_not_configured) + + r = client.post( + "/api/v5/checkpoint/extract", + json={"content": _sample_markdown()}, + ) + assert r.status_code == 412, r.text + detail = r.json().get("detail") + assert isinstance(detail, dict), f"detail must be a structured dict, got {type(detail)}" + assert detail.get("error") == "provider_not_configured" + assert "not configured" in detail.get("message", "").lower() + assert detail.get("provider") # backend echoes which provider it tried + assert isinstance(detail.get("fix"), list) + assert len(detail["fix"]) >= 3, "fix list should enumerate at least 3 paths" + # CRITICAL: response body must NOT contain mock content. The bug we are + # fixing literally returned "Mock decision from provider" in the response. + body_text = r.text + assert "Mock decision from provider" not in body_text + assert "Mock Checkpoint" not in body_text + + +# ── Default path with a real provider: provider/model echoed ───────────────── + + +class _FakeAdapter(ProviderAdapter): + """Test double for a real provider — returns valid JSON shaped like a + real extraction so we can verify the endpoint's success-path metadata.""" + + def send(self, messages, model, **kwargs): + return json.dumps({ + "title": "Real Extraction Result", + "objective": "Build envdiff CLI.", + "summary": "A short summary of the design doc.", + "decisions": ["Use argparse, not click"], + "assumptions": ["Python 3.11+ is available"], + "tasks": [{"id": "impl-1", "text": "Scaffold the CLI"}], + "open_questions": [], + "entities": ["envdiff"], + "artifacts": [], + }) + + def healthcheck(self) -> bool: # pragma: no cover + return True + + +def test_extract_with_provider_echoes_provider_and_model(client, monkeypatch): + """Default path + a real (faked) provider: 200 OK, response echoes + `provider` and `model` so the CLI can show 'extracted via openai/gpt-4o-mini' + on commit and refuse to persist mock content.""" + def _fake_adapter(provider: str, allow_mock: bool = False): + return _FakeAdapter() + + monkeypatch.setattr(checkpoint_route, "get_adapter", _fake_adapter) + r = client.post( "/api/v5/checkpoint/extract", json={"content": _sample_markdown()}, ) assert r.status_code == 200, r.text data = r.json() - # Response shape: every field present, strings are strings, lists are lists. - assert isinstance(data["title"], str) - assert isinstance(data["objective"], str) - assert isinstance(data["summary"], str) - assert isinstance(data["decisions"], list) - assert isinstance(data["assumptions"], list) - assert isinstance(data["tasks"], list) - assert isinstance(data["open_questions"], list) - assert isinstance(data["entities"], list) - assert isinstance(data["artifacts"], list) - # Something must have been extracted from the sample — the title must be - # non-empty and at least one decision should appear since the sample has - # a "## Decisions" section with two bullet points. - assert data["title"].strip() != "" - assert len(data["decisions"]) > 0 + assert data["title"] == "Real Extraction Result" + # Echo: must be the configured provider/model, NOT "mock". + assert data["provider"] != "mock", "real-provider path must not echo provider=mock" + assert data["provider"] != "" + assert data["model"] != "mock" + assert data["model"] != "" + + +# ── Validation tests (unchanged) ───────────────────────────────────────────── def test_extract_rejects_empty_content(client): diff --git a/cli/smriti_cli/formatters.py b/cli/smriti_cli/formatters.py index 41aa6c4..0f14a75 100644 --- a/cli/smriti_cli/formatters.py +++ b/cli/smriti_cli/formatters.py @@ -953,7 +953,16 @@ def format_doctor(report: dict) -> str: else: parts.append("- missing capabilities: none") parts.append(f"- CLI path: {checks.get('cli_path') or 'unknown'}") - parts.append(f"- background provider: {checks.get('background_provider') or 'unknown'}") + bg = checks.get("background_provider") or "unknown" + if bg == "ready": + parts.append("- background provider: ready (real LLM extraction enabled)") + elif bg == "mock_or_disabled": + parts.append( + "- background provider: ⚠ MOCK or DISABLED — `smriti checkpoint create " + "--extract`, draft, and review will fail until a provider is configured" + ) + else: + parts.append(f"- background provider: {bg}") parts.append("") parts.append("## Capabilities") diff --git a/cli/smriti_cli/main.py b/cli/smriti_cli/main.py index eff1d31..97c0b7d 100644 --- a/cli/smriti_cli/main.py +++ b/cli/smriti_cli/main.py @@ -87,6 +87,29 @@ def _fail(message: str, code: int = 1) -> None: sys.exit(code) +def _fail_provider_not_configured(detail: dict, code: int = 78) -> None: + """Render the backend's structured 'provider_not_configured' error as a + clear, actionable message and exit non-zero. + + Exit code 78 = EX_CONFIG (configuration error) so scripts can distinguish + a missing-provider failure from a generic CLI error. + """ + provider = detail.get("provider") or "background" + msg = detail.get("message") or ( + f"Background LLM provider '{provider}' is not configured." + ) + fix = detail.get("fix") or [] + lines: list[str] = [] + lines.append("error: " + msg) + lines.append("") + lines.append("To fix this, do one of:") + for item in fix: + lines.append(f" - {item}") + lines.append("") + lines.append("Run `smriti doctor` to confirm provider status after configuring.") + _fail("\n".join(lines), code=code) + + def _confirm(preview: str, yes_flag: bool) -> bool: """Interactive 'Type yes' if stdin is a TTY, otherwise require --yes. @@ -799,13 +822,42 @@ def _read_checkpoint_json(args: argparse.Namespace) -> dict: def cmd_doctor(client: SmritiClient, args: argparse.Namespace) -> None: - """Print narrow backend/runtime diagnostics.""" + """Print narrow backend/runtime diagnostics. + + With --strict, exits non-zero if any critical check fails. The current + strict checks are: + - backend reachable + - background provider configured for real LLM extraction (--extract, + draft, review). When this fails strict, agents and CI should refuse + to use --extract; manual JSON checkpointing still works. + """ report = _build_doctor_report(client) if args.json: _print_json(report) else: print(format_doctor(report), end="") + if getattr(args, "strict", False): + failures: list[str] = [] + backend = report.get("backend") or {} + if not backend.get("reachable"): + failures.append("backend is not reachable") + checks = report.get("checks") or {} + bg = checks.get("background_provider") + if bg == "mock_or_disabled": + failures.append( + "background provider is mock_or_disabled — `smriti checkpoint create " + "--extract`, draft, and review will fail. Configure a provider." + ) + elif bg == "unknown": + failures.append("background provider status is unknown (backend reachable?).") + if failures: + print("", file=sys.stderr) + print("strict mode: " + str(len(failures)) + " check(s) failed:", file=sys.stderr) + for f in failures: + print(f" - {f}", file=sys.stderr) + sys.exit(78) # EX_CONFIG + def cmd_space_list(client: SmritiClient, args: argparse.Namespace) -> None: spaces = client.list_spaces() @@ -1169,7 +1221,24 @@ def cmd_checkpoint_create(client: SmritiClient, args: argparse.Namespace) -> Non # and use the returned fields as the commit payload. No hand-written # JSON required. content = _read_raw_content() - extracted = client.extract_checkpoint_content(content) + try: + extracted = client.extract_checkpoint_content(content) + except SmritiError as e: + # Provider not configured: backend returns 412 with a structured + # detail. Surface the actionable fix list and exit non-zero — do + # NOT fall back to mock or hand-written content silently. + if e.status == 412 and isinstance(e.detail, dict) and e.detail.get("error") == "provider_not_configured": + _fail_provider_not_configured(e.detail) + raise + # Defense in depth: if the backend somehow returned mock content on + # the default path (use_mock=False), refuse to commit it. Mock + # output committed into a real project pollutes reasoning state. + if extracted.get("provider") == "mock": + _fail( + "error: extract returned mock content (provider=mock) on the default path.\n" + "Refusing to commit. This is a backend bug — please report it.\n" + "If you intentionally want mock content (tests/demos), build the payload manually.", + ) # Extractor returns `title`; checkpoints store `message`. Map it. # If the LLM returned an empty title, fall back to a generic label # so the required `message` field is always populated. @@ -1184,12 +1253,17 @@ def cmd_checkpoint_create(client: SmritiClient, args: argparse.Namespace) -> Non "entities": extracted.get("entities", []), "artifacts": extracted.get("artifacts", []), } + # Stash provider/model so we can show them on commit confirmation. + _extract_provider = extracted.get("provider") or "" + _extract_model = extracted.get("model") or "" else: payload = _read_checkpoint_json(args) if not isinstance(payload, dict): _fail("Checkpoint JSON must be an object, got: " + type(payload).__name__) if not payload.get("message"): _fail("Checkpoint JSON must include a 'message' field.") + _extract_provider = "" + _extract_model = "" if args.dry_run: # Print the full payload (extracted or hand-written) as JSON and @@ -1260,6 +1334,8 @@ def cmd_checkpoint_create(client: SmritiClient, args: argparse.Namespace) -> Non else: h = commit.get("commit_hash", "") print(f"Created checkpoint: `{h[:7]}` {commit.get('message', '')}") + if _extract_provider: + print(f" extracted via {_extract_provider}/{_extract_model}") def cmd_checkpoint_show(client: SmritiClient, args: argparse.Namespace) -> None: @@ -2124,6 +2200,15 @@ def _build_parser() -> argparse.ArgumentParser: help="Diagnose backend reachability and runtime/code freshness", ) doctor_parser.add_argument("--json", action="store_true", help="Output structured JSON") + doctor_parser.add_argument( + "--strict", + action="store_true", + help=( + "Exit non-zero (EX_CONFIG / 78) when any critical check fails — " + "currently: backend unreachable, or background provider mock_or_disabled " + "(blocks `checkpoint create --extract`, draft, review)." + ), + ) doctor_parser.set_defaults(func=cmd_doctor) # quickstart — seed a curated demo space so the product clicks fast diff --git a/cli/smriti_cli/skill_pack/template.md b/cli/smriti_cli/skill_pack/template.md index d4416d5..e0d3398 100644 --- a/cli/smriti_cli/skill_pack/template.md +++ b/cli/smriti_cli/skill_pack/template.md @@ -585,6 +585,40 @@ know about. Concretely: - **You are about to explore an alternative direction and want to preserve the current line.** Fork first, then checkpoint on the fork. +### 4.1 Before your first `--extract`: verify the provider + +The extract path uses Smriti's background LLM to turn freeform markdown +into structured checkpoint fields. It **requires a real configured +provider** (OpenAI / Anthropic / OpenRouter / generic OpenAI-compatible +endpoint). If no provider is configured, the extract endpoint returns +**HTTP 412 Precondition Failed** rather than silently falling back to +mock content — mock content committed into a real Space pollutes the +reasoning state Smriti exists to keep trustworthy. + +Run `smriti doctor` once at the start of a session that intends to use +`--extract`, and read the `background provider:` line. + +``` +{{mcp:Bash: `smriti doctor` (or ask the human to run it — `doctor` is CLI-only, no MCP tool).}}{{cli:smriti doctor}} +``` + +Look for: + +- **`background provider: ready (real LLM extraction enabled)`** — + good, use `--extract` freely. +- **`background provider: ⚠ MOCK or DISABLED — ... will fail until a + provider is configured`** — do **not** use `--extract`. Either ask + the human to configure a provider (OpenAI/Anthropic/OpenRouter key, + or a local OpenAI-compatible endpoint via the generic provider), or + write the checkpoint **manually as JSON** (see 4.1 below). Do not + attempt to commit mock content as if it were real reasoning state. + +For scripted/CI use, `smriti doctor --strict` exits non-zero +(EX_CONFIG / 78) when the background provider is mock/disabled — +useful as a pre-flight gate before any `--extract` step. + +### 4.2 The extract path (provider required) + Use the **extract path**, not hand-written JSON. Pass freeform markdown describing the inflection point and Smriti's background LLM will pull out the structured fields (message, objective, summary, decisions, @@ -630,7 +664,7 @@ Always tag `author_agent` with a stable identifier for your agent agents know who wrote what on the shared timeline. Inconsistent or missing `author_agent` makes divergence unattributable. -### 4.1 Checkpoint notes: annotating without checkpointing +### 4.3 Checkpoint notes: annotating without checkpointing Sometimes you need to add context to an existing checkpoint without creating a new one. Checkpoint notes are additive annotations — diff --git a/website/index.html b/website/index.html index 7f55b22..13c5390 100644 --- a/website/index.html +++ b/website/index.html @@ -579,7 +579,7 @@

Try it

Up and running in minutes.

-

No Docker, no API keys, no cloud required. The core coordination loop runs on a local SQLite file. API keys come in only for optional LLM-assisted features.

+

No Docker, no API keys, no cloud required for the core coordination loop — setup, doctor, quickstart, state/current/metrics, claims, attach, and hand-written checkpoints all run on a local SQLite file. LLM-backed features — checkpoint create --extract, draft, review, the chat send loop — need a real provider (OpenAI / Anthropic / OpenRouter / local OpenAI-compatible). They refuse to run on the default mock adapter rather than silently committing placeholder content.