Add checkpoint notes: additive founder annotations with kind (note/milestone/noise)

This commit is contained in:
Himanshu Dongre 2026-04-13 19:10:37 +05:30
parent a3b570e609
commit 9b109ae94c
6 changed files with 201 additions and 0 deletions

View file

@ -388,3 +388,82 @@ Rules:
except Exception as e:
logger.error(f"Extract LLM call failed: {e}")
raise HTTPException(status_code=502, detail=f"Extract failed: {e}")
# ── Checkpoint notes ─────────────────────────────────────────────────────────
from pydantic import BaseModel, Field
from datetime import datetime, timezone
VALID_NOTE_KINDS = {"note", "milestone", "noise"}
class AddNoteRequest(BaseModel):
text: str = Field(..., min_length=1, max_length=2000)
author: str = Field(default="founder", max_length=100)
kind: str = Field(default="note")
class NoteResponse(BaseModel):
id: str
author: str
text: str
kind: str
created_at: str
checkpoint_id: str
@router.post("/{checkpoint_id}/notes", response_model=NoteResponse, status_code=201)
def add_checkpoint_note(
checkpoint_id: uuid.UUID,
payload: AddNoteRequest,
db: Session = Depends(get_db),
):
"""Add an additive note to a checkpoint without modifying its immutable fields.
Notes are stored in the checkpoint's metadata_ JSONB field under the
'notes' key. The checkpoint's decisions, summary, artifacts, and all
other fields remain untouched. Notes are append-only in v1.
"""
commit = db.get(CommitModel, checkpoint_id)
if not commit:
raise HTTPException(status_code=404, detail="Checkpoint not found")
if payload.kind not in VALID_NOTE_KINDS:
raise HTTPException(
status_code=400,
detail=f"Invalid kind '{payload.kind}'. Must be one of: {', '.join(sorted(VALID_NOTE_KINDS))}",
)
note_id = str(uuid.uuid4())
now = datetime.now(timezone.utc).isoformat()
note = {
"id": note_id,
"author": payload.author,
"text": payload.text,
"kind": payload.kind,
"created_at": now,
}
# Append to existing notes array in metadata_, creating it if absent.
meta = dict(commit.metadata_ or {})
notes = list(meta.get("notes", []))
notes.append(note)
meta["notes"] = notes
commit.metadata_ = meta
# Force SQLAlchemy to detect the JSONB mutation.
from sqlalchemy.orm.attributes import flag_modified
flag_modified(commit, "metadata_")
db.commit()
return NoteResponse(
id=note_id,
author=payload.author,
text=payload.text,
kind=payload.kind,
created_at=now,
checkpoint_id=str(checkpoint_id),
)

View file

@ -209,6 +209,22 @@ class SmritiClient:
params["include_expired"] = "true"
return self._request("GET", "/api/v5/claims", params=params)
# ── Checkpoint notes ──────────────────────────────────────────────
def add_checkpoint_note(
self,
checkpoint_id: str,
text: str,
author: str = "founder",
kind: str = "note",
) -> dict:
"""POST /api/v5/checkpoint/{id}/notes — append a note."""
return self._request(
"POST",
f"/api/v5/checkpoint/{checkpoint_id}/notes",
json={"text": text, "author": author, "kind": kind},
)
# ── Branch disposition ────────────────────────────────────────────
def close_branch(self, space_id: str, branch_name: str, disposition: str) -> dict:

View file

@ -392,6 +392,20 @@ def format_checkpoint(commit: dict, *, full_artifacts: bool = False) -> str:
if entities:
parts.append(f"## Entities\n{', '.join(entities)}\n")
# Notes from metadata_ — additive founder/human annotations.
metadata = commit.get("metadata") or commit.get("metadata_") or {}
notes = metadata.get("notes") or []
if notes:
lines = ["## Notes"]
for n in notes:
kind = n.get("kind", "note")
kind_prefix = f"[{kind}] " if kind != "note" else ""
author = n.get("author", "?")
created = _relative_time(n.get("created_at") or "")
text = n.get("text", "")
lines.append(f"- {kind_prefix}{author} · {created}{text}")
parts.append("\n".join(lines) + "\n")
return "\n".join(p for p in parts if p).rstrip() + "\n"

View file

@ -417,6 +417,21 @@ def cmd_checkpoint_delete(client: SmritiClient, args: argparse.Namespace) -> Non
print(f"Deleted checkpoint `{commit['commit_hash'][:7]}`{note}.")
def cmd_checkpoint_note(client: SmritiClient, args: argparse.Namespace) -> None:
"""Add a note to a checkpoint."""
result = client.add_checkpoint_note(
checkpoint_id=args.checkpoint_id,
text=args.text,
author=args.author,
kind=args.kind,
)
if args.json:
_print_json(result)
else:
kind_label = f" [{result['kind']}]" if result['kind'] != 'note' else ""
print(f"Note added to checkpoint `{result['checkpoint_id'][:8]}…`{kind_label}")
def cmd_fork(client: SmritiClient, args: argparse.Namespace) -> None:
# Fetch the checkpoint first to derive space_id. This also gives us the
# source message for the output line so the user sees what they forked.
@ -995,6 +1010,21 @@ def _build_parser() -> argparse.ArgumentParser:
cp_delete.add_argument("--json", action="store_true", help="Output structured JSON")
cp_delete.set_defaults(func=cmd_checkpoint_delete)
cp_note = cp_sub.add_parser(
"note",
help="Add a note to a checkpoint (additive, does not modify checkpoint fields)",
)
cp_note.add_argument("checkpoint_id", help="Checkpoint UUID to annotate")
cp_note.add_argument("--text", required=True, help="Note text (max 2000 chars)")
cp_note.add_argument("--author", default="founder", help="Author name (default: founder)")
cp_note.add_argument(
"--kind", default="note",
choices=["note", "milestone", "noise"],
help="Note kind (default: note)",
)
cp_note.add_argument("--json", action="store_true")
cp_note.set_defaults(func=cmd_checkpoint_note)
# fork (top-level: crosses checkpoint → session)
fork_parser = subparsers.add_parser(
"fork",

View file

@ -470,6 +470,39 @@ def smriti_delete_space(space: str) -> str:
return f"Deleted space '{s['name']}' and its {len(commits)} checkpoint(s).\n"
@mcp.tool()
def smriti_checkpoint_note(
checkpoint_id: str,
text: str,
author: str = "founder",
kind: str = "note",
) -> str:
"""Add a note to a checkpoint without modifying its immutable fields.
Notes are additive context founder commentary, milestone markers,
or noise labels stored alongside the checkpoint. The checkpoint's
decisions, summary, artifacts, and all other fields remain untouched.
Args:
checkpoint_id: Checkpoint UUID to annotate.
text: Note text (max 2000 chars).
author: Author name (default: founder).
kind: One of: note (default), milestone, noise.
"""
client = _client()
try:
result = client.add_checkpoint_note(
checkpoint_id=checkpoint_id,
text=text,
author=author,
kind=kind,
)
except SmritiError as e:
_raise_from(e)
kind_label = f" [{result['kind']}]" if result['kind'] != 'note' else ""
return f"Note added to checkpoint `{result['checkpoint_id'][:8]}…`{kind_label}"
@mcp.tool()
def smriti_close_branch(
space: str,

View file

@ -235,6 +235,35 @@ export function CommitDetailPage() {
{!commit.objective && !commit.summary && commit.tasks.length === 0 && (
<p className="text-gray-600 text-sm italic">No structured state recorded in this commit.</p>
)}
{/* Notes — additive founder annotations */}
{commit.metadata?.notes && commit.metadata.notes.length > 0 && (
<FieldBlock label="Notes">
<div className="space-y-2">
{commit.metadata.notes.map((n: any, i: number) => (
<div key={n.id || i} className="text-sm">
<div className="flex items-center gap-2 mb-0.5">
{n.kind && n.kind !== 'note' && (
<span className={`text-[9px] font-medium px-1.5 py-0.5 rounded-full border ${
n.kind === 'milestone'
? 'text-amber-400 border-amber-500/30 bg-amber-900/20'
: n.kind === 'noise'
? 'text-gray-500 border-gray-600 bg-gray-800/30'
: ''
}`}>
{n.kind}
</span>
)}
<span className="text-[10px] text-gray-600">
{n.author} · {new Date(n.created_at).toLocaleString()}
</span>
</div>
<p className="text-gray-300">{n.text}</p>
</div>
))}
</div>
</FieldBlock>
)}
</div>
{/* Delta (state changes since parent) */}