Polish external validation output and docs

This commit is contained in:
Himanshu Dongre 2026-05-17 12:15:14 +05:30
parent 2ac2abcfae
commit 76f6f4c2c7
14 changed files with 144 additions and 28 deletions

View file

@ -73,8 +73,8 @@ One project, one Smriti Space, multiple agents. Each reads the state, declares i
## Getting started
You will need: Python 3.11+ and Node 18+. Docker is only needed for
Postgres/shared-team mode.
You will need: Python 3.11+ and Node 20.19+ or Node 22.12+. Docker is
only needed for Postgres/shared-team mode.
### 1. Clone and set up for solo/local mode
@ -146,11 +146,15 @@ This creates the space, installs skill packs for Claude Code and Codex, and conf
**MCP config** (Claude Code, Cursor, Windsurf):
`smriti init` prints the exact config for your install. If you configure
MCP manually, activate the Smriti venv first and use the absolute path from
`which smriti-mcp` so the host does not pick up a stale executable.
```json
{
"mcpServers": {
"smriti": {
"command": "smriti-mcp",
"command": "/absolute/path/to/smriti-mcp",
"env": { "SMRITI_API_URL": "http://localhost:8000" }
}
}

View file

@ -156,7 +156,7 @@ def _normalize_task(raw: object) -> Optional[CurrentTask]:
if not text:
return None
status = raw.get("status") or "open"
intent = raw.get("intent_hint")
intent = raw.get("intent_hint") or raw.get("intent_type")
return CurrentTask(
text=text,
id=(raw.get("id") or None),

View file

@ -171,9 +171,12 @@ def get_space_metrics(space_id: uuid.UUID, db: Session = Depends(get_db)):
total_decisions += len(decisions)
total_tasks += len(tasks)
# Structured tasks: at least one task is a dict with intent_hint
# Structured tasks: at least one task is a dict-shaped task entry.
# Intent hints are useful, but task IDs without intent hints are still
# structured tasks and should not make the human-facing metric look
# contradictory.
has_structured = any(
isinstance(t, dict) and t.get("intent_hint")
isinstance(t, dict) and str(t.get("text") or "").strip()
for t in tasks
)
if has_structured:

View file

@ -176,6 +176,31 @@ def test_current_open_tasks_grouped_by_intent(client):
assert cur["counts"]["open_tasks"] == 3 # done task excluded
def test_current_open_tasks_accept_legacy_intent_type(client):
"""Demo data seeded before intent_hint should still group cleanly."""
repo_id = _create_repo(client, "Legacy Task Intent")
session_id = _create_session(client, repo_id)
_commit(
client,
repo_id,
session_id,
message="Has legacy task intent",
tasks=[
{
"text": "Implement middleware",
"intent_type": "implement",
"id": "middleware",
"status": "open",
},
],
)
cur = _get_current(client, repo_id)
assert set(cur["open_tasks_by_intent"].keys()) == {"implement"}
assert cur["open_tasks_by_intent"]["implement"][0]["id"] == "middleware"
def test_current_recent_milestones(client):
"""Milestone notes surface in recent_milestones; plain notes do not."""
repo_id = _create_repo(client, "Milestones")

View file

@ -143,12 +143,14 @@ def test_metrics_structured_tasks_and_ids(client):
tasks=[{"text": "Task", "intent_hint": "implement"}])
_commit(client, repo_id, sid, "with ids",
tasks=[{"text": "Task", "id": "t1", "intent_hint": "test"}])
_commit(client, repo_id, sid, "with id but no intent",
tasks=[{"text": "Task", "id": "t2"}])
r = client.get(f"/api/v5/metrics/spaces/{repo_id}")
data = r.json()
assert data["state_quality"]["checkpoints_with_structured_tasks"] == 2
assert data["state_quality"]["checkpoints_with_task_ids"] == 1
assert data["state_quality"]["checkpoints_with_structured_tasks"] == 3
assert data["state_quality"]["checkpoints_with_task_ids"] == 2
def test_metrics_response_shape(client):

View file

@ -36,11 +36,15 @@ Run Smriti as a local MCP server so agents inside Claude Code, Cursor, or Windsu
**Claude Code config** (typically `~/.config/claude-code/mcp.json` or `~/Library/Application Support/Claude/claude_desktop_config.json` — check your host's docs for the exact path):
`smriti init <space>` prints the recommended MCP config for the current
install. If you configure MCP manually, use the absolute path from
`which smriti-mcp` after activating the intended environment.
```json
{
"mcpServers": {
"smriti": {
"command": "smriti-mcp",
"command": "/absolute/path/to/smriti-mcp",
"env": { "SMRITI_API_URL": "http://localhost:8000" }
}
}

View file

@ -128,7 +128,10 @@ def _normalize_task_item(item) -> dict:
if isinstance(item, str):
return {"text": item}
if isinstance(item, dict) and item.get("text"):
return item
task = dict(item)
if not task.get("intent_hint") and task.get("intent_type"):
task["intent_hint"] = task["intent_type"]
return task
# Fallback: coerce to string
return {"text": str(item)}
@ -499,7 +502,7 @@ def _direction_text(value) -> str:
if isinstance(value, str):
return value.strip()
if isinstance(value, dict):
for key in ("text", "summary", "objective", "message"):
for key in ("text", "objective", "headline", "summary", "message"):
raw = value.get(key)
if isinstance(raw, str) and raw.strip():
return raw.strip()
@ -867,7 +870,10 @@ def format_metrics(data: dict) -> str:
agent_dist = coord.get("agent_checkpoints", {})
dist_str = ", ".join(f"{a}: {n}" for a, n in sorted(agent_dist.items()))
parts.append("## Coordination")
parts.append(f"{total} checkpoints · {agents} agent{'s' if agents != 1 else ''} ({dist_str})")
agent_summary = f"{total} checkpoints · {agents} agent{'s' if agents != 1 else ''}"
if dist_str:
agent_summary += f" ({dist_str})"
parts.append(agent_summary)
cross = coord.get("cross_agent_continuations", 0)
parts.append(f"{cross} cross-agent continuation{'s' if cross != 1 else ''}")
@ -889,7 +895,12 @@ def format_metrics(data: dict) -> str:
noise = sq.get("noise_count", 0)
parts.append("## State quality")
parts.append(f"{avg_d} decisions/checkpoint · {avg_t} tasks/checkpoint")
parts.append(f"{structured} with structured tasks · {with_ids} with task IDs")
structured_label = "checkpoint" if structured == 1 else "checkpoints"
ids_label = "checkpoint" if with_ids == 1 else "checkpoints"
parts.append(
f"{structured} {structured_label} with structured tasks · "
f"{with_ids} {ids_label} with task IDs"
)
parts.append(f"{milestones} milestone{'s' if milestones != 1 else ''} · {noise} noise label{'s' if noise != 1 else ''}")
parts.append("")

View file

@ -78,9 +78,9 @@ _ASSUME_SINGLE_INSTANCE = "The API runs as a single instance for now"
_ASSUME_MULTI_INSTANCE = "The API will scale to multiple instances within two quarters"
def _task(task_id: str, text: str, intent_type: str, status: str) -> dict:
def _task(task_id: str, text: str, intent_hint: str, status: str) -> dict:
"""A structured task entry — the shape the extractor and `state` expect."""
return {"id": task_id, "text": text, "intent_type": intent_type, "status": status}
return {"id": task_id, "text": text, "intent_hint": intent_hint, "status": status}
# ── Main-branch checkpoints ─────────────────────────────────────────────────
@ -109,8 +109,8 @@ MAIN_CHECKPOINTS: list[dict] = [
"Do we limit per client IP, or per API key?",
],
"tasks": [
_task("survey", "Survey rate-limiting algorithms and pick candidates", "explore", "open"),
_task("decide", "Decide the algorithm and what the limit is keyed on", "decide", "open"),
_task("survey", "Survey rate-limiting algorithms and pick candidates", "investigate", "open"),
_task("decide", "Decide the algorithm and what the limit is keyed on", "investigate", "open"),
_task("middleware", "Build the rate-limit middleware", "implement", "open"),
],
"entities": ["public API", "API gateway", "rate limiter"],
@ -132,8 +132,8 @@ MAIN_CHECKPOINTS: list[dict] = [
"assumptions": [_ASSUME_BURST, _ASSUME_SINGLE_INSTANCE],
"open_questions": [],
"tasks": [
_task("survey", "Survey rate-limiting algorithms and pick candidates", "explore", "done"),
_task("decide", "Decide the algorithm and what the limit is keyed on", "decide", "done"),
_task("survey", "Survey rate-limiting algorithms and pick candidates", "investigate", "done"),
_task("decide", "Decide the algorithm and what the limit is keyed on", "investigate", "done"),
_task("middleware", "Build the rate-limit middleware", "implement", "open"),
_task("loadtest", "Load-test burst and sustained traffic", "test", "open"),
],
@ -169,8 +169,8 @@ MAIN_CHECKPOINTS: list[dict] = [
"assumptions": [_ASSUME_BURST, _ASSUME_SINGLE_INSTANCE],
"open_questions": [],
"tasks": [
_task("survey", "Survey rate-limiting algorithms and pick candidates", "explore", "done"),
_task("decide", "Decide the algorithm and what the limit is keyed on", "decide", "done"),
_task("survey", "Survey rate-limiting algorithms and pick candidates", "investigate", "done"),
_task("decide", "Decide the algorithm and what the limit is keyed on", "investigate", "done"),
_task("middleware", "Build the rate-limit middleware", "implement", "done"),
_task("loadtest", "Load-test burst and sustained traffic", "test", "open"),
],
@ -217,8 +217,8 @@ MAIN_CHECKPOINTS: list[dict] = [
"assumptions": [_ASSUME_BURST, _ASSUME_SINGLE_INSTANCE],
"open_questions": [],
"tasks": [
_task("survey", "Survey rate-limiting algorithms and pick candidates", "explore", "done"),
_task("decide", "Decide the algorithm and what the limit is keyed on", "decide", "done"),
_task("survey", "Survey rate-limiting algorithms and pick candidates", "investigate", "done"),
_task("decide", "Decide the algorithm and what the limit is keyed on", "investigate", "done"),
_task("middleware", "Build the rate-limit middleware", "implement", "done"),
_task("loadtest", "Load-test burst and sustained traffic", "test", "done"),
_task("document", "Document the 429 / Retry-After contract for API consumers", "docs", "open"),
@ -259,9 +259,9 @@ FORK_CHECKPOINT: dict = {
"assumptions": [_ASSUME_BURST, _ASSUME_MULTI_INSTANCE],
"open_questions": ["Is ~3ms of added per-request latency acceptable at the edge?"],
"tasks": [
_task("survey", "Survey rate-limiting algorithms and pick candidates", "explore", "done"),
_task("decide", "Decide the algorithm and what the limit is keyed on", "decide", "done"),
_task("redis-spike", "Prototype the Redis token-bucket store", "explore", "done"),
_task("survey", "Survey rate-limiting algorithms and pick candidates", "investigate", "done"),
_task("decide", "Decide the algorithm and what the limit is keyed on", "investigate", "done"),
_task("redis-spike", "Prototype the Redis token-bucket store", "investigate", "done"),
],
"entities": ["rate limiter", "token bucket", "Redis", "distributed rate limiter"],
"artifacts": [],

View file

@ -146,6 +146,20 @@ def test_format_project_current_renders_contract_sections():
assert "## Recent activity" in out
def test_format_project_current_prefers_objective_for_direction():
payload = _current_payload()
payload["current_direction"] = {
"objective": "Add per-client rate limiting before launch.",
"headline": "Load test passes",
"summary": "Long backend-provided summary that may be preview-clipped.",
}
out = format_project_current(payload)
assert "Add per-client rate limiting before launch." in out
assert "Long backend-provided summary" not in out
def test_cmd_current_prefers_backend_payload(capsys: pytest.CaptureFixture[str]):
client = MagicMock(spec=SmritiClient)
client.resolve_space.return_value = _space()
@ -238,4 +252,3 @@ def test_cmd_current_falls_back_to_shipped_endpoints(capsys: pytest.CaptureFixtu
client.get_space_state.assert_called_once_with("space-uuid")
client.list_commits.assert_called_once_with("space-uuid")
client.get_space_metrics.assert_called_once_with("space-uuid")

View file

@ -0,0 +1,33 @@
from __future__ import annotations
from smriti_cli.formatters import format_metrics
def test_format_metrics_omits_empty_agent_distribution():
out = format_metrics({
"space_name": "empty",
"coordination": {
"total_checkpoints": 0,
"unique_agents": 0,
"agent_checkpoints": {},
},
"state_quality": {},
"branches": {},
})
assert "0 checkpoints · 0 agents\n" in out
assert "0 agents ()" not in out
def test_format_metrics_names_checkpoint_task_counts():
out = format_metrics({
"space_name": "demo",
"coordination": {},
"state_quality": {
"checkpoints_with_structured_tasks": 5,
"checkpoints_with_task_ids": 5,
},
"branches": {},
})
assert "5 checkpoints with structured tasks · 5 checkpoints with task IDs" in out

View file

@ -150,6 +150,12 @@ def test_fixture_claim_intent_types_are_backend_valid():
assert claim["intent_type"] in BACKEND_INTENT_TYPES
def test_fixture_task_intent_hints_are_backend_valid():
for cp in [*MAIN_CHECKPOINTS, FORK_CHECKPOINT]:
for task in cp["tasks"]:
assert task["intent_hint"] in BACKEND_INTENT_TYPES
def test_fixture_has_one_done_and_one_active_claim():
statuses = {claim["final_status"] for claim in CLAIMS}
assert statuses == {"done", "active"}
@ -180,7 +186,7 @@ def test_fixture_branch_diverges_from_its_fork_source():
def test_fixture_tasks_are_well_formed():
for cp in [*MAIN_CHECKPOINTS, FORK_CHECKPOINT]:
for task in cp["tasks"]:
assert {"id", "text", "intent_type", "status"} <= set(task)
assert {"id", "text", "intent_hint", "status"} <= set(task)
assert task["status"] in {"open", "done"}

View file

@ -529,6 +529,15 @@ def test_task_section_structured_with_intent():
assert "- Add freshness tests [test]" in out
def test_task_section_accepts_legacy_intent_type():
"""Older demo/checkpoint data with intent_type still renders as structured."""
tasks = [
{"text": "Update docs", "intent_type": "docs"},
]
out = _task_section(tasks)
assert "- Update docs [docs]" in out
def test_task_section_structured_with_blocked_by():
"""Structured task with blocked_by renders inline marker."""
tasks = [

View file

@ -28,6 +28,9 @@
"typescript": "~5.9.3",
"typescript-eslint": "^8.57.0",
"vite": "^8.0.1"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@babel/code-frame": {

View file

@ -3,6 +3,9 @@
"private": true,
"version": "0.0.0",
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",