mirror of
https://github.com/himanshudongre/smriti.git
synced 2026-08-28 05:14:59 +00:00
Polish external validation output and docs
This commit is contained in:
parent
2ac2abcfae
commit
76f6f4c2c7
14 changed files with 144 additions and 28 deletions
10
README.md
10
README.md
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("")
|
||||
|
||||
|
|
|
|||
|
|
@ -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": [],
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
33
cli/tests/test_metrics_cli.py
Normal file
33
cli/tests/test_metrics_cli.py
Normal 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
|
||||
|
|
@ -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"}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
3
frontend/package-lock.json
generated
3
frontend/package-lock.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue