mirror of
https://github.com/himanshudongre/smriti.git
synced 2026-08-28 05:14:59 +00:00
Add checkpoint notes: additive founder annotations with kind (note/milestone/noise)
This commit is contained in:
parent
a3b570e609
commit
9b109ae94c
6 changed files with 201 additions and 0 deletions
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) */}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue