A fresh Smriti install with no provider configured could still run
`smriti checkpoint create --extract`, which silently returned
MockAdapter content like "Mock decision from provider". If committed,
that placeholder text became part of the user's real reasoning state.
Root cause: backend/app/api/routes/checkpoint.py:417 called
`get_adapter(cfg.background.provider, allow_mock=True)`. The
`allow_mock=True` flag means the registry quietly returns MockAdapter
when no API key is configured, rather than raising. The CLI received
the canned mock JSON and committed it without inspecting whether it
came from a real LLM.
The extract endpoint was the only route in the codebase with this
pattern — draft, review, chat title, and chat send already correctly
pass `allow_mock=False`.
The new contract:
- Core Smriti (setup, doctor, quickstart, state/current/metrics,
claims, attach, manual JSON checkpoints) requires no API key.
- Real LLM-backed paths (`--extract`, draft, review, chat send)
require a configured provider — OpenAI / Anthropic / OpenRouter /
generic OpenAI-compatible (local models like Ollama).
- Mock extraction still works for tests and demos, but only when the
caller explicitly opts in (use_mock=true on the HTTP payload).
It is never silently the default.
Backend:
- POST /api/v5/checkpoint/extract now passes allow_mock=False and
catches ProviderNotConfiguredError, returning HTTP 412 with a
structured detail: error code, human message, the provider it
tried, and a list of fix paths the CLI surfaces.
- CheckpointExtractResponse gains `provider` and `model` echo fields
(additive, default empty) so callers can confirm what answered.
CLI:
- checkpoint create --extract catches 412 and prints the actionable
fix list; exits 78 (EX_CONFIG). Defense in depth: even on a 200,
refuses to commit if response.provider == "mock" on the default
path (so any future regression in the backend is still caught).
- smriti doctor surfaces background provider state prominently:
`ready (real LLM extraction enabled)` or `⚠ MOCK or DISABLED — …
will fail until a provider is configured`.
- smriti doctor --strict exits 78 when the background provider is
mock/disabled or the backend is unreachable. Safe to wire into
CI before any --extract step.
- On a successful --extract commit, the CLI shows `extracted via
<provider>/<model>` under the commit confirmation.
Docs:
- README: new "Provider configuration (LLM-backed features)" section
drawing the boundary explicitly; mentions the generic provider for
local OpenAI-compatible models; flags mock as test-only.
- .env.example: rewrote the provider section so an empty key or a
model-without-a-key is not interpreted as "ready".
- Skill pack template: new §4.1 "Before your first --extract: verify
the provider" telling agents to run `smriti doctor`, refuse
--extract when background_provider is mock/disabled, and fall back
to manual JSON checkpoints or ask the human to configure a
provider. Re-rendered to AGENTS.md (Codex target). The Claude Code
target (.claude/skills/smriti/SKILL.md) is gitignored per-user
install; rerun `smriti skills install claude-code` to refresh.
- website/index.html: Try-it lede now spells out which features need
a provider rather than gesturing at "optional LLM features".
Tests:
- test_extract_without_provider_fails_loud: regression for the bug —
monkeypatches get_adapter to raise ProviderNotConfiguredError,
asserts HTTP 412 with the structured detail shape, and asserts
the response body contains neither "Mock decision from provider"
nor "Mock Checkpoint". This test would fail on pre-fix code.
- test_extract_with_provider_echoes_provider_and_model: pins the
green path — provider and model must be echoed and must not be
"mock" when the real adapter answers.
- test_extract_happy_path_with_mock: unchanged, still pins the
explicit use_mock=true contract.
- Full backend integration suite: 165 passed locally (with the
pre-existing real-provider draft test passing under
backend/config/providers.yaml).
Skill pack (template.md -> AGENTS.md, .claude/skills/smriti/SKILL.md):
- §3.2 Repo reconciliation: lead with `## Repo state`'s automated drift
signals; keep the manual git log checks as the documented fallback
for checkpoints without recorded git state.
- §3.5 Backend reachability: lead with `smriti doctor` as the
structured health check; keep `curl /health` as the raw fallback.
- Bump skill pack version 2.4 -> 2.5; re-render AGENTS.md (the
.claude/skills/smriti/SKILL.md install is local-only and untracked).
- test_skill_pack: bump the expected version; add four single-line
required phrases proving the new content shipped (`Repo state`,
`smriti doctor`, "ahead of the last checkpoint", "checkpoint taken
on a different branch").
docs/API.md:
- POST /api/v4/chat/commit: add `repo_state` to the request body
example and a paragraph explaining its purpose (drift detection),
persistence (`context_blob`), and read-back path (V4 state endpoint).
- DELETE /api/v2/repos/{repo_id}: add the missing section with the
`force` query param, 204/404/409 responses, and the structured
detail shape for the 409 refusal.
CLI suite: 253 passed. AGENTS.md and SKILL.md verified byte-identical
to fresh render(target) output.
The repo-state section added by the previous commit shows working-tree
and git-upstream drift. It does not answer the question Smriti's trust
story actually needs: has the repo moved since the last checkpoint?
Record git provenance on checkpoints and compare against it. The CLI
captures the git HEAD and branch when it creates a checkpoint; the V4
commit endpoint stores that under the commit's context_blob and returns
it on read. `smriti state` then compares the working repo against the
latest checkpoint's recorded HEAD/branch and surfaces:
- repo unchanged since the last checkpoint
- repo is N commit(s) ahead — recorded state may be stale
- repo history has diverged from the last checkpoint
- the last checkpoint was taken on a different branch
All local and read-only: no fetch, no reconciliation. Checkpoints made
before this feature (or by the MCP server) record no git state and are
simply left uncompared.
The deletion-safety thread guarded the CLI (`--force`) and the MCP tool
(`confirm_space`), but `DELETE /api/v2/repos/{id}` itself still cascade-
deleted a fully populated space — every checkpoint, session, and turn —
with no server-side check. Any client that bypassed the CLI/MCP guards
(a direct API call, the web UI, a future client) reopened the incident
path.
Gate the route: `delete_repo` now counts the space's checkpoints and
refuses with 409 plus a structured detail (`message`, `checkpoint_count`,
`requires_force`) unless `?force=true` is passed — the same shape as the
`checkpoint delete` dependent-guard. An empty space still deletes with no
force.
Wire the existing clients through the new signal so their behavior is
unchanged: the CLI passes `force=args.force` (its own `--force` gate),
the MCP tool passes `force=true` after its `confirm_space` gate, and
quickstart passes `force=true` to tear down its own demo space.
A destructive-operations incident: `smriti space delete <space> -y`
cascade-deleted a fully populated space. `-y` skipped the only gate (the
confirmation prompt), and `cmd_space_delete` never checked how much the
space held — so one flag irreversibly deleted the space and every
checkpoint, session, and turn under it.
Gate destructive deletes in `cmd_space_delete`: a space that holds
checkpoints, or that the current repo is attached to (.smriti.json), now
requires an explicit `--force` — `-y` alone is refused, with a message
naming the reason and the flag. `--force` and `-y` stay orthogonal, the
same shape as `checkpoint delete --cascade`. Empty, unattached spaces keep
the existing `-y` convenience. CLI-side only; no backend change.
Smriti was repo-local wiring: every command needed an explicit <space>, and
the SessionStart hook hard-coded the space name, so working across projects
and sessions meant re-stating the space constantly.
Add a `.smriti.json` attachment file at the repo root binding the repo to a
space. The CLI resolves `<space>` from it — walking up from the working
directory the way git finds `.git` — so `<space>` is now optional on the
everyday commands (state, current, metrics, claim, checkpoint, branch,
worktree). `smriti attach <space>` is the explicit binding verb (`smriti
attach` with no argument shows the current binding); `smriti init` also
writes the attachment. The generated SessionStart hook is now space-agnostic
— `smriti state --compact` resolves from the attachment, so the hook is
identical for every project and survives re-attaching.
`space delete` still requires an explicit space. No backend changes —
attach/init reuse the existing set_project_root endpoint.
`smriti current` already normalizes a list-valued blocked_by to a clean
comma-separated string via the backend CurrentTask validator, but `smriti
state` rendered it through an f-string and leaked the raw Python list repr
(['a', 'b']) into the state brief — the surface agents read at session start.
Add a `_coerce_blocked_by` helper in the CLI formatters mirroring that
validator, and apply it in `_normalize_task_item` so every `smriti state`
task line renders blocked_by the same way `smriti current` does. Adds
regression tests.
A fresh install opens to an empty space, so Smriti's value — which only
shows once reasoning has accumulated — is invisible on day one. `smriti
quickstart` seeds one small, finished project (a rate-limiting feature
built by two agents, with a branch explored and dropped) and prints a
short guided walkthrough.
The demo space `smriti-demo` carries a marker in its description: --remove
only deletes a marked space, --reset rebuilds it, and seeding rolls back a
half-built space on failure. The fixture is plain structured data with
content-integrity tests guarding counts, intent types, note kinds, the
demo marker, and branch divergence.
Adds WorkTree schema and migration, /api/v5/worktrees CRUD, CLI and MCP worktree surfaces, targeted regression tests, health capability, and minimal docs. Live Postgres migration/manual localhost verification intentionally remain pending until the full backend provider-config gate is resolved.
Tasks gain an optional id field (short slug like "impl-1", "docs-arch").
Claims gain an optional task_id field referencing a specific task. The
state brief shows (id: X) on tasks and (task: X) on claims, making it
precise which task a claim covers.
Skill pack v1.9 teaches the recheck pattern: after creating a claim,
re-read state to detect if another agent claimed the same task_id in
the race window. If collision detected, abandon and pivot.
This solves the near-simultaneous start problem from the autonomy
validation where both agents picked [docs] because claims had no
task-level identity.
The health endpoint now returns git_sha and a capabilities list so
agents can detect when the running backend is missing features they
need (e.g., claims, structured_tasks). Skill pack v1.8 teaches the
capabilities probe: check /health when a 404 or missing section
suggests the backend is stale, tell the human to restart.
Diagnosed from the autonomy validation where Codex hit a backend
without /api/v5/claims — the backend process was running old code.
Tasks in checkpoints evolve from flat strings to objects with optional
intent_hint (implement/review/investigate/docs/test), blocked_by
(dependency label), and status (open/done). Agents reading the state
brief can now self-select complementary work by matching task intents
against active claim intent_types — no founder routing needed.
Backward-compatible: old string tasks normalize at render time. No
schema migration. JSONB handles both shapes. Skill pack v1.7 teaches
the autonomous selection reflex.