Add Project Current State CLI surface

This commit is contained in:
Himanshu Dongre 2026-05-16 19:32:03 +05:30
parent 7c8f259342
commit 704f28570c
5 changed files with 654 additions and 0 deletions

View file

@ -136,6 +136,8 @@ smriti state <space> # multi-branch continua
smriti state <space> --preview # truncate artifacts to a short preview
smriti state <space> --main-only # legacy single-HEAD path (pre-V4 behaviour)
smriti state <space> --json # structured output
smriti current <space> # compact Project Current State surface
smriti current <space> --json # structured current-state payload
smriti claim create <space> --agent <name> --scope "..." # declare work intent before starting
smriti claim create <space> --agent <name> --scope "..." --intent-type review

View file

@ -192,6 +192,16 @@ class SmritiClient:
params=params if params else None,
)
def get_current_state(self, space_id: str) -> dict:
"""GET /api/v5/current/spaces/{space_id} — Project Current State.
This endpoint is the backend-owned compact operational payload for
founder-facing and agent-facing current-state surfaces. CLI callers
may fall back to composing the same contract from existing endpoints
while older backends are still deployed.
"""
return self._request("GET", f"/api/v5/current/spaces/{space_id}")
# ── Work claims ──────────────────────────────────────────────────
def create_claim(

View file

@ -100,6 +100,13 @@ def _short_hash(commit_hash: str | None) -> str:
return commit_hash[:7] if commit_hash else "?"
def _truncate_text(text: str, limit: int = 180) -> str:
"""Keep compact surfaces compact without hiding that content exists."""
if len(text) <= limit:
return text
return text[: limit - 1].rstrip() + ""
def _list_section(heading: str, items: list[str]) -> str:
if not items:
return ""
@ -478,6 +485,220 @@ def format_state_brief(
return result
def _direction_text(value) -> str:
"""Normalize current_direction to a readable paragraph.
The backend contract may keep `current_direction` as a string or a richer
object. Keep the formatter tolerant so the CLI can ship in parallel with
the backend implementation.
"""
if isinstance(value, str):
return value.strip()
if isinstance(value, dict):
for key in ("text", "summary", "objective", "message"):
raw = value.get(key)
if isinstance(raw, str) and raw.strip():
return raw.strip()
return ""
def _format_current_counts(counts: dict | None) -> str:
if not counts:
return "## Counts\nNo count data available.\n"
labels = {
"checkpoints": "checkpoints",
"active_claims": "active claims",
"active_branches": "active branches",
"open_tasks": "open tasks",
"milestones": "milestones",
"attention": "attention signals",
}
ordered = [
"checkpoints",
"active_claims",
"active_branches",
"open_tasks",
"milestones",
"attention",
]
bits: list[str] = []
for key in ordered:
if key in counts and counts.get(key) is not None:
bits.append(f"{labels[key]}: {counts[key]}")
for key in sorted(k for k in counts.keys() if k not in labels):
bits.append(f"{key.replace('_', ' ')}: {counts[key]}")
return "## Counts\n" + (" · ".join(bits) if bits else "No count data available.") + "\n"
def _format_attention(attention: list | None) -> str:
lines = ["## Needs attention"]
if not attention:
lines.append("- none")
return "\n".join(lines) + "\n"
for raw in attention:
if isinstance(raw, str):
lines.append(f"- {raw}")
continue
if isinstance(raw, dict):
severity = raw.get("severity") or raw.get("kind")
message = raw.get("message") or raw.get("text") or raw.get("label")
if not message:
message = str(raw)
prefix = f"[{severity}] " if severity else ""
lines.append(f"- {prefix}{message}")
continue
lines.append(f"- {raw}")
return "\n".join(lines) + "\n"
def _format_current_active_work(active_work: list | None) -> str:
lines = ["## Active work"]
if not active_work:
lines.append("No active work claims.")
return "\n".join(lines) + "\n"
for item in active_work:
if not isinstance(item, dict):
lines.append(f"- {item}")
continue
agent = item.get("agent") or "unknown"
intent = item.get("intent_type") or item.get("intent") or "implement"
branch = item.get("branch_name") or item.get("branch") or "main"
scope = item.get("scope") or item.get("message") or "(no scope)"
task_id = item.get("task_id")
created = item.get("claimed_at") or item.get("created_at")
rel = f" · {_relative_time(created)}" if created else ""
task = f" task `{task_id}`" if task_id else ""
lines.append(
f"- `{agent}` [{intent}]{task} on `{branch}`{rel}{scope}"
)
return "\n".join(lines) + "\n"
def _format_recent_milestones(milestones: list | None) -> str:
lines = ["## Recent milestones"]
if not milestones:
lines.append("No recent milestones.")
return "\n".join(lines) + "\n"
for item in milestones:
if not isinstance(item, dict):
lines.append(f"- {item}")
continue
h = item.get("commit_hash") or item.get("checkpoint_hash") or ""
hash_part = f"`{_short_hash(h)}`"
created = item.get("created_at")
rel = f" · {_relative_time(created)}" if created else ""
text = (
item.get("note")
or item.get("text")
or item.get("message")
or "(milestone)"
)
author = item.get("author") or item.get("author_agent")
author_part = f" · `{author}`" if author else ""
lines.append(f"- {hash_part}{author_part}{rel}{_truncate_text(str(text))}")
return "\n".join(lines) + "\n"
def _format_open_tasks_by_intent(open_tasks_by_intent: dict | None) -> str:
lines = ["## Open tasks by intent"]
if not open_tasks_by_intent:
lines.append("No open tasks.")
return "\n".join(lines) + "\n"
rendered_any = False
for intent in sorted(open_tasks_by_intent.keys()):
tasks = open_tasks_by_intent.get(intent) or []
if not tasks:
continue
rendered_any = True
lines.append(f"### {intent}")
for raw in tasks:
task = _normalize_task_item(raw)
text = task.get("text", "")
task_id = task.get("id")
blocked = task.get("blocked_by")
id_part = f"`{task_id}` " if task_id else ""
blocked_part = f" → blocked by: {blocked}" if blocked else ""
lines.append(f"- {id_part}{text}{blocked_part}")
if not rendered_any:
lines.append("No open tasks.")
return "\n".join(lines) + "\n"
def _format_recent_activity(activity: list | None) -> str:
lines = ["## Recent activity"]
if not activity:
lines.append("No recent activity.")
return "\n".join(lines) + "\n"
for item in activity:
if not isinstance(item, dict):
lines.append(f"- {item}")
continue
h = item.get("commit_hash") or item.get("checkpoint_hash") or ""
author = item.get("author_agent") or item.get("author") or "unknown"
branch = item.get("branch_name") or item.get("branch")
created = item.get("created_at")
rel = f" · {_relative_time(created)}" if created else ""
branch_part = f" on `{branch}`" if branch and branch != "main" else ""
msg = item.get("message") or item.get("title") or "(no message)"
lines.append(
f"- `{_short_hash(h)}` · `{author}`{branch_part}{rel}{_truncate_text(str(msg), 140)}"
)
return "\n".join(lines) + "\n"
def format_project_current(data: dict) -> str:
"""Readable Project Current State surface.
Contract keys:
space_id, name, description, current_direction, counts, attention,
active_work, recent_milestones, open_tasks_by_intent, recent_activity.
"""
name = data.get("name") or data.get("space_name") or "Untitled space"
parts: list[str] = [f"# {name} — current state\n"]
if data.get("description"):
parts.append(str(data["description"]).rstrip() + "\n")
direction = _direction_text(data.get("current_direction"))
if direction:
parts.append(direction + "\n")
else:
parts.append("No current direction recorded.\n")
latest = data.get("latest_checkpoint")
if isinstance(data.get("current_direction"), dict):
latest = latest or data["current_direction"].get("latest_checkpoint")
if not latest and data.get("recent_activity"):
latest = data["recent_activity"][0]
if isinstance(latest, dict):
h = latest.get("commit_hash") or latest.get("checkpoint_hash") or ""
msg = latest.get("message") or "(no message)"
author = latest.get("author_agent") or latest.get("author")
created = latest.get("created_at")
meta = [f"Latest checkpoint: `{_short_hash(h)}`"]
if author:
meta.append(f"by `{author}`")
if created:
meta.append(_relative_time(created))
parts.append(" · ".join(meta) + f"{msg}\n")
parts.append(_format_current_counts(data.get("counts")))
parts.append(_format_attention(data.get("attention")))
parts.append(_format_current_active_work(data.get("active_work")))
parts.append(_format_recent_milestones(data.get("recent_milestones")))
parts.append(_format_open_tasks_by_intent(data.get("open_tasks_by_intent")))
parts.append(_format_recent_activity(data.get("recent_activity")))
return "\n".join(p for p in parts if p).rstrip() + "\n"
def format_checkpoint(commit: dict, *, full_artifacts: bool = False) -> str:
"""Readable markdown for a single checkpoint."""
parts: list[str] = []

View file

@ -8,6 +8,7 @@ Commands for agent and programmatic use:
smriti space delete <space> [-y]
smriti doctor
smriti state <space> [--preview]
smriti current <space>
smriti fork <checkpoint-id> [--branch <name>]
smriti restore <checkpoint-id>
smriti compare <checkpoint-a> <checkpoint-b>
@ -62,6 +63,7 @@ from .formatters import (
format_doctor,
format_fork_result,
format_metrics,
format_project_current,
format_restore_brief,
format_review,
format_space_list,
@ -427,6 +429,175 @@ def _print_no_checkpoints(space: dict, args: argparse.Namespace) -> None:
print("No checkpoints yet. Create one with `smriti checkpoint create`.")
def _current_task_item(raw: Any) -> dict:
if isinstance(raw, str):
return {"text": raw}
if isinstance(raw, dict):
return raw
return {"text": str(raw)}
def _current_open_tasks_by_intent(tasks: list) -> dict[str, list[dict]]:
grouped: dict[str, list[dict]] = {}
for raw in tasks:
task = _current_task_item(raw)
status = task.get("status", "open")
if status and status != "open":
continue
intent = task.get("intent_hint") or task.get("intent_type") or "other"
grouped.setdefault(intent, []).append(task)
return grouped
def _current_milestones(commits: list[dict], limit: int = 5) -> list[dict]:
milestones: list[dict] = []
for commit in commits:
metadata = commit.get("metadata") or commit.get("metadata_") or {}
notes = metadata.get("notes") or []
for note in notes:
if note.get("kind") != "milestone":
continue
milestones.append({
"commit_hash": commit.get("commit_hash"),
"message": commit.get("message"),
"note": note.get("text") or commit.get("message"),
"author": note.get("author") or commit.get("author_agent"),
"created_at": note.get("created_at") or commit.get("created_at"),
})
if len(milestones) >= limit:
return milestones
# Lineage/current endpoints may expose only note kind summaries. When
# the note text is unavailable, still surface the checkpoint as a
# milestone so the current-state view does not hide the marker.
note_kinds = commit.get("note_kinds") or []
if "milestone" in note_kinds:
milestones.append({
"commit_hash": commit.get("commit_hash"),
"message": commit.get("message"),
"note": commit.get("message"),
"author_agent": commit.get("author_agent"),
"created_at": commit.get("created_at"),
})
if len(milestones) >= limit:
return milestones
return milestones
def _current_activity(commits: list[dict], limit: int = 5) -> list[dict]:
return [
{
"id": c.get("id"),
"commit_hash": c.get("commit_hash"),
"message": c.get("message"),
"author_agent": c.get("author_agent"),
"branch_name": c.get("branch_name"),
"created_at": c.get("created_at"),
}
for c in commits[:limit]
]
def _build_current_payload_from_existing(client: SmritiClient, space: dict) -> dict:
"""Compose the Project Current State contract from shipped endpoints.
This keeps the CLI usable while the backend-owned compact endpoint rolls
out in parallel. Once the endpoint is present, `cmd_current` will prefer
it and skip this compatibility path.
"""
space_id = space["id"]
state = client.get_space_state(space_id)
commits = client.list_commits(space_id)
try:
metrics = client.get_space_metrics(space_id)
except SmritiError as e:
if e.status not in (404, 405):
raise
metrics = {}
commit = state.get("commit") or {}
active_work = state.get("active_claims") or []
active_branches = state.get("active_branches") or []
open_tasks = _current_open_tasks_by_intent(commit.get("tasks") or [])
open_task_count = sum(len(items) for items in open_tasks.values())
milestones = _current_milestones(commits)
attention: list[dict] = []
if not commit:
attention.append({
"severity": "setup",
"message": "No checkpoints yet; create the first checkpoint to establish project direction.",
})
divergence = state.get("divergence") or {}
if divergence.get("pairs"):
attention.append({
"severity": "risk",
"message": "Active branch divergence detected; run `smriti compare` before reconciling.",
})
if active_branches:
attention.append({
"severity": "branch",
"message": f"{len(active_branches)} active branch(es) need disposition when resolved.",
})
open_questions = commit.get("open_questions") or []
if open_questions:
attention.append({
"severity": "question",
"message": f"{len(open_questions)} open question(s) on the latest checkpoint.",
})
if open_task_count > 0 and not active_work:
attention.append({
"severity": "next",
"message": f"{open_task_count} open task(s) are available with no active claim.",
})
coord = metrics.get("coordination") or {}
state_quality = metrics.get("state_quality") or {}
branch_metrics = metrics.get("branches") or {}
counts = {
"checkpoints": coord.get("total_checkpoints", len(commits)),
"active_claims": len(active_work),
"active_branches": branch_metrics.get("active", len(active_branches)),
"open_tasks": open_task_count,
"milestones": state_quality.get("milestone_count", len(milestones)),
"attention": len(attention),
}
return {
"space_id": space_id,
"name": space.get("name"),
"description": space.get("description") or "",
"current_direction": (
commit.get("objective")
or commit.get("summary")
or commit.get("message")
or ""
),
"counts": counts,
"attention": attention,
"active_work": active_work,
"recent_milestones": milestones,
"open_tasks_by_intent": open_tasks,
"recent_activity": _current_activity(commits),
}
def cmd_current(client: SmritiClient, args: argparse.Namespace) -> None:
"""Print the compact Project Current State surface for a space."""
space = client.resolve_space(args.space)
try:
data = client.get_current_state(space["id"])
except SmritiError as e:
if e.status not in (404, 405):
raise
data = _build_current_payload_from_existing(client, space)
if args.json:
_print_json(data)
else:
print(format_project_current(data), end="")
def cmd_checkpoint_create(client: SmritiClient, args: argparse.Namespace) -> None:
space = client.resolve_space(args.space)
@ -1229,6 +1400,15 @@ def _build_parser() -> argparse.ArgumentParser:
state_parser.add_argument("--json", action="store_true", help="Output structured JSON")
state_parser.set_defaults(func=cmd_state)
# current — compact founder/agent current-state surface
current_parser = subparsers.add_parser(
"current",
help="Print the compact Project Current State surface for a space",
)
current_parser.add_argument("space", help="Space name or UUID")
current_parser.add_argument("--json", action="store_true", help="Output structured JSON")
current_parser.set_defaults(func=cmd_current)
# checkpoint
cp_parser = subparsers.add_parser("checkpoint", help="Manage checkpoints")
cp_sub = cp_parser.add_subparsers(dest="subcommand", required=True)

View file

@ -0,0 +1,241 @@
from __future__ import annotations
import argparse
from datetime import datetime, timezone
from unittest.mock import MagicMock
import pytest
from smriti_cli import main as cli_main
from smriti_cli.client import SmritiClient, SmritiError
from smriti_cli.formatters import format_project_current
def _space() -> dict:
return {
"id": "space-uuid",
"name": "smriti-dev",
"description": "Shared reasoning backend.",
}
def _commit(**overrides) -> dict:
base = {
"id": "checkpoint-uuid",
"repo_id": "space-uuid",
"commit_hash": "abcdef1234567890",
"branch_name": "main",
"author_agent": "codex-local",
"message": "Project Current State direction",
"objective": "Make current project operation legible.",
"summary": "Current-state surface is the next product packaging layer.",
"tasks": [
{
"id": "current-cli",
"text": "Add Project Current State CLI surface",
"intent_hint": "implement",
},
{
"id": "current-review",
"text": "Review the current-state UI",
"intent_hint": "review",
"blocked_by": "current-ui",
},
],
"open_questions": ["Should this live in state or current?"],
"created_at": datetime.now(timezone.utc).isoformat(),
}
base.update(overrides)
return base
def _current_payload() -> dict:
return {
"space_id": "space-uuid",
"name": "smriti-dev",
"description": "Shared reasoning backend.",
"current_direction": "Make current project operation legible.",
"latest_checkpoint": {
"commit_hash": "abcdef1234567890",
"message": "Project Current State direction",
"author_agent": "codex-local",
"created_at": datetime.now(timezone.utc).isoformat(),
},
"counts": {
"checkpoints": 74,
"active_claims": 1,
"active_branches": 0,
"open_tasks": 2,
"milestones": 2,
"attention": 1,
},
"attention": [
{"severity": "question", "message": "1 open question on latest checkpoint."}
],
"active_work": [
{
"agent": "codex-local",
"intent_type": "implement",
"task_id": "current-cli",
"branch_name": "codex/project-current-state-cli",
"scope": "Add Project Current State CLI surface",
"claimed_at": datetime.now(timezone.utc).isoformat(),
}
],
"recent_milestones": [
{
"commit_hash": "1111222233334444",
"note": "First autonomous task selection worked.",
"author": "founder",
"created_at": datetime.now(timezone.utc).isoformat(),
}
],
"open_tasks_by_intent": {
"implement": [
{
"id": "current-cli",
"text": "Add Project Current State CLI surface",
"intent_hint": "implement",
}
],
"review": [
{
"id": "current-review",
"text": "Review the current-state UI",
"intent_hint": "review",
"blocked_by": "current-ui",
}
],
},
"recent_activity": [
{
"commit_hash": "abcdef1234567890",
"author_agent": "codex-local",
"message": "Project Current State direction",
"created_at": datetime.now(timezone.utc).isoformat(),
}
],
}
def test_current_parser_wiring():
parser = cli_main._build_parser()
args = parser.parse_args(["current", "smriti-dev"])
assert args.command == "current"
assert args.space == "smriti-dev"
assert args.func is cli_main.cmd_current
def test_format_project_current_renders_contract_sections():
out = format_project_current(_current_payload())
assert "# smriti-dev — current state" in out
assert "## Counts" in out
assert "active claims: 1" in out
assert "## Needs attention" in out
assert "[question] 1 open question" in out
assert "## Active work" in out
assert "task `current-cli`" in out
assert "## Recent milestones" in out
assert "First autonomous task selection worked." in out
assert "## Open tasks by intent" in out
assert "### implement" in out
assert "`current-review` Review the current-state UI → blocked by: current-ui" in out
assert "## Recent activity" in out
def test_cmd_current_prefers_backend_payload(capsys: pytest.CaptureFixture[str]):
client = MagicMock(spec=SmritiClient)
client.resolve_space.return_value = _space()
client.get_current_state.return_value = _current_payload()
args = argparse.Namespace(space="smriti-dev", json=False)
cli_main.cmd_current(client, args)
out = capsys.readouterr().out
assert "# smriti-dev — current state" in out
assert "Add Project Current State CLI surface" in out
client.resolve_space.assert_called_once_with("smriti-dev")
client.get_current_state.assert_called_once_with("space-uuid")
client.get_space_state.assert_not_called()
def test_cmd_current_json_outputs_backend_payload():
client = MagicMock(spec=SmritiClient)
client.resolve_space.return_value = _space()
payload = _current_payload()
client.get_current_state.return_value = payload
args = argparse.Namespace(space="smriti-dev", json=True)
captured: list[dict] = []
original = cli_main._print_json
cli_main._print_json = captured.append
try:
cli_main.cmd_current(client, args)
finally:
cli_main._print_json = original
assert captured == [payload]
def test_cmd_current_falls_back_to_shipped_endpoints(capsys: pytest.CaptureFixture[str]):
client = MagicMock(spec=SmritiClient)
client.resolve_space.return_value = _space()
client.get_current_state.side_effect = SmritiError(
"endpoint missing", status=404
)
commit = _commit()
client.get_space_state.return_value = {
"commit": commit,
"head": {"commit_id": "checkpoint-uuid"},
"active_claims": [
{
"agent": "codex-local",
"intent_type": "implement",
"task_id": "current-cli",
"branch_name": "codex/project-current-state-cli",
"scope": "Add Project Current State CLI surface",
"claimed_at": datetime.now(timezone.utc).isoformat(),
}
],
"active_branches": [],
"divergence": None,
}
client.list_commits.return_value = [
{
**commit,
"metadata": {
"notes": [
{
"kind": "milestone",
"text": "Current-state direction chosen.",
"author": "founder",
"created_at": datetime.now(timezone.utc).isoformat(),
}
]
},
}
]
client.get_space_metrics.return_value = {
"coordination": {"total_checkpoints": 74},
"state_quality": {"milestone_count": 3},
"branches": {"active": 0},
}
args = argparse.Namespace(space="smriti-dev", json=False)
cli_main.cmd_current(client, args)
out = capsys.readouterr().out
assert "Make current project operation legible." in out
assert "checkpoints: 74" in out
assert "active claims: 1" in out
assert "open tasks: 2" in out
assert "[question] 1 open question" in out
assert "Current-state direction chosen." in out
assert "### implement" in out
assert "`current-cli` Add Project Current State CLI surface" in out
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")