Add capabilities manifest to /health for stale-backend detection

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.
This commit is contained in:
Himanshu Dongre 2026-04-13 23:06:34 +05:30
parent bd257c3393
commit 6da93ae856
4 changed files with 102 additions and 14 deletions

View file

@ -1,10 +1,29 @@
import logging
import pathlib
import re
import subprocess
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.config import settings
def _resolve_git_sha() -> str:
"""Return the short git SHA of the backend directory, or 'unknown'."""
try:
result = subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
cwd=pathlib.Path(__file__).resolve().parent.parent,
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0:
return result.stdout.strip()
except Exception:
pass
return "unknown"
class SecretGuardFilter(logging.Filter):
def filter(self, record):
if isinstance(record.msg, str) and re.search(r'sk-[a-zA-Z0-9_\-]+', record.msg):
@ -48,9 +67,29 @@ def create_app() -> FastAPI:
app.include_router(lineage.router, prefix="/api/v5", tags=["lineage-v5"])
app.include_router(claims.router, prefix="/api/v5", tags=["claims-v5"])
# ── Capabilities manifest ────────────────────────────────────────
# Computed once at startup so /health is zero-cost at request time.
_git_sha = _resolve_git_sha()
# Hardcoded feature flags matching the route modules included above.
# When a new feature ships (new route module, new query param, new
# schema shape), add it here so agents can detect stale backends.
_capabilities = [
"claims", # /api/v5/claims
"structured_tasks", # task objects with intent_hint/blocked_by/status
"checkpoint_notes", # /api/v5/checkpoint/{id}/notes
"branch_disposition", # PATCH /api/v5/lineage/branches/disposition
"freshness", # since_commit_id on state endpoint
"compact_state", # --compact mode on state brief
]
@app.get("/health")
async def health_check():
return {"status": "ok"}
return {
"status": "ok",
"git_sha": _git_sha,
"capabilities": _capabilities,
}
@app.on_event("startup")
async def startup_event():

View file

@ -0,0 +1,38 @@
"""Integration tests for GET /health capabilities manifest.
Validates that the health endpoint returns the capabilities list and
git_sha that agents use to detect stale backends.
"""
def test_health_returns_capabilities(client):
"""Health endpoint includes status, git_sha, and capabilities."""
r = client.get("/health")
assert r.status_code == 200
data = r.json()
assert data["status"] == "ok"
assert "git_sha" in data
assert isinstance(data["git_sha"], str)
assert len(data["git_sha"]) > 0
assert "capabilities" in data
assert isinstance(data["capabilities"], list)
def test_health_includes_required_capabilities(client):
"""All shipped features are listed in capabilities."""
r = client.get("/health")
data = r.json()
caps = data["capabilities"]
required = [
"claims",
"structured_tasks",
"checkpoint_notes",
"branch_disposition",
"freshness",
"compact_state",
]
for cap in required:
assert cap in caps, f"Missing capability: {cap}"

View file

@ -1,5 +1,5 @@
---
smriti_skill_pack_version: 1.7
smriti_skill_pack_version: 1.8
title: Smriti — how to use it well
target: {{display_name}}
---
@ -214,7 +214,7 @@ Say out loud: **"Session complete. Branch pushed, checkpoint
written, working tree clean."** or **"Stopping — checkpointed
findings, branch is [disposition]."**
### 3.5 Backend reachability
### 3.5 Backend reachability and capabilities
The Smriti backend is a shared service started by the human. You
are a client of it. You do not own it.
@ -231,14 +231,22 @@ are a client of it. You do not own it.
tool loop creates environment-variable inheritance issues that
cause silent mock fallback on all LLM-backed endpoints. The
human starts the backend; you use it.
- **Runtime freshness after code changes.** If backend code has
been merged to main since the backend was last started (e.g.,
new API routes, schema changes, config fixes), the backend
must be restarted before agents can rely on the new endpoints.
Check `git log --oneline -5` against the running server's
behavior. If a new endpoint returns 404 or the behavior does
not match the merged code, tell the human: "The backend may
need a restart to pick up recent changes on main."
- **Check capabilities before using advanced features.** After
reading state, before creating claims or using features like
structured tasks, probe the backend:
{{mcp:`curl -s http://localhost:8000/health`}}{{cli:`curl -s http://localhost:8000/health`}}
The response includes `git_sha` and a `capabilities` list. If
you need `claims` but the capabilities list does not include it,
the backend is running stale code. Tell the human: "The backend
at localhost:8000 does not support [feature]. Its git_sha is
[sha] but the current repo is at [repo sha]. Please restart
the backend with `make dev` to pick up recent changes."
- **When to check capabilities:** You do NOT need to check on every
session. Check when:
- A Smriti API call returns 404 on a route you expect to exist
- The state brief is missing sections you expect (e.g., no
`## Active work` when you know claims were recently merged)
- You are about to use a feature for the first time in a session
### 3.6 Work claims: declare intent before working
@ -825,7 +833,7 @@ tell you. Do not guess.
---
*Smriti skill pack version {{primary_mode}}-1.7 — this file is
*Smriti skill pack version {{primary_mode}}-1.8 — this file is
authoritative for agent behaviour on this project. If you catch it
contradicting itself or your observed behaviour of the tools, tell
the human; the skill pack is versioned and meant to be updated.*

View file

@ -44,7 +44,7 @@ def test_load_template_nonempty():
def test_get_version_parses_frontmatter():
version = get_version()
assert version == "1.7"
assert version == "1.8"
def test_get_version_raises_when_frontmatter_missing():
@ -142,8 +142,11 @@ _REQUIRED_PHRASES = [
"clean finish",
"branch disposition",
"push your branch",
# Section 3.5 — backend reachability
# Section 3.5 — backend reachability and capabilities
"backend reachability",
"capabilities",
"git_sha",
"stale code",
"do not attempt to start",
# Section 3.6 — work claims
"work claims",