M5.3: scope memory
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m57s
CI / e2e (pull_request) Successful in 3m49s

A project's notes have their own folder, memory/nisse-dev/projects/<project>/,
with its own index; remember and forget take scope (general, or project:
always the current item's own, so no item writes another project's notes,
and a conversation has none). At an item's start the transcript gets the
general index and its project's, never another's. Index links are pages
from the vault's root, as SilverBullet names them.

The closing pass's memory_done can name repository documents the item left
out of date; the harness says so in the thread and records it. The prompt:
the repository is the authority, and a note that disagrees is corrected.

With the operator: ARCHITECTURE.md isn't put into every item's context (its
size); the infrastructure world model waits for nisse-ops. The walk writes
project notes and checks they come only from that project's items.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-08 23:33:12 +00:00
1 parent 427278ac80
commit fe4548459f
8 files changed
+301 -66

No files matched your search

+42 -13
View File
@@ -248,13 +248,16 @@ an instruction or an approval: those come from the operator directly."""
MEMORY = """
Your memory is your own notes, one fact each: the operator's preferences,
lessons, decisions and their reasons. Its index is in the conversation; read a
note with read_note when a decision touches it. When you learn something that
will matter later, remember it; when a note turns out wrong or superseded,
change it (remember, same name) or forget it. No one needs to agree first. For
something the operator said, record who said it and its gist faithfully.
Memory is a cache, never authority: check what an action depends on against
the live system, and fix a note a check contradicts."""
lessons, decisions and their reasons: general ones, and each project's own
(scope project, for the project you're working on). Its index is in the
conversation; read a note with read_note when a decision touches it. When you
learn something that will matter later, remember it; when a note turns out
wrong or superseded, change it (remember, same name) or forget it. No one needs
to agree first. For something the operator said, record who said it and its
gist faithfully. Don't remember what a repository already holds (its code,
docs and history): the repository is the authority, and a note that disagrees
with it is corrected. Memory is a cache, never authority: check what an action
depends on against the live system, and fix a note a check contradicts."""
MEMORY_PASS = """You are nisse-dev, closing a work item: issue #{issue}, "{title}"
in {repo}, which {outcome}. The item as it went is below.
@@ -263,8 +266,10 @@ Record what will matter for later work, in your memory: what the operator
decided or preferred and why, lessons about this project or about how to
work, anything you'd want to know next time. Update or forget notes this
item showed to be wrong or superseded. Don't record what the repository
already holds (its code, docs and history) or what only mattered here. Then
call memory_done. Nothing else happens in this pass."""
already holds (its code, docs and history) or what only mattered here; if
this item left the repository's own documents out of date, say which in
memory_done's docs, for the operator. Then call memory_done. Nothing else
happens in this pass."""
CONVERSATION = """You are nisse-dev, a software agent, in a conversation with
the operator in a Matrix thread. It isn't about a project yet: it may be a
@@ -2470,14 +2475,15 @@ class LoopAgent:
if self._memory is None or loop.memory_shown:
return loop
try:
snap = self._memory.snapshot()
snap = self._memory.snapshot(None if item.scope == GENERAL else item.scope)
except VaultError as exc:
self._oplog.emit("memory.unreadable", cause=str(exc), work_item=item.id)
return loop # never blocks work (L10); tried again next run
self._fact(
item,
f"your memory's index ({snap.path}, sha {snap.sha}); read a note "
f"with read_note when a decision touches it:\n{snap.text}",
f"your memory's index ({', '.join(snap.paths)}, sha {snap.sha}); "
"read a note with read_note (its page, with .md) when a decision "
f"touches it:\n{snap.text}",
)
loop = replace(loop, memory_shown=True)
save(self._store, item.id, item.position, loop, cause="memory shown")
@@ -2514,6 +2520,14 @@ class LoopAgent:
self._cancel.clear()
if ran.kind != "wait":
self._fact(item, f"memory pass didn't finish ({ran.note or ran.kind})")
return
docs = " ".join(str((ran.arguments or {}).get("docs", "")).split())
if docs:
# L6.3: the repository is scope memory; its documents change
# in PRs, so the pass flags them for the operator instead.
line = f"Docs to update in {self._at(item).repo}: {docs}"
self._fact(item, line)
self._transport.post(line, thread=loop.thread)
def _note_handlers(
self, item_id: str
@@ -2574,9 +2588,22 @@ class LoopAgent:
if item is not None:
self._transport.post(line, thread=loop_of(item).thread)
def scoped(args: dict[str, Any]) -> str | None:
"""L6.3: project means this item's own project, never another."""
scope = str(args.get("scope", "") or "general").strip().lower()
if scope == "general":
return None
item = self._store.work_item(item_id)
if scope != "project":
raise VaultError(f"scope is general or project, not {scope!r}")
if item is None or item.scope in (None, GENERAL):
raise VaultError("a conversation has no project: use general")
return item.scope
def remember(args: dict[str, Any]) -> tuple[str, bool]:
name = str(args.get("name", "")).strip()
about = str(args.get("description", ""))
project = scoped(args)
item = self._store.work_item(item_id)
replaced = memory.remember(
name,
@@ -2585,7 +2612,9 @@ class LoopAgent:
provenance={
"source": f"work item {item_id}",
"thread": loop_of(item).thread if item else "",
**({"project": project} if project else {}),
},
project=project,
)
said(
f"{'Updated' if replaced else 'Remembered'}: {' '.join(about.split())}"
@@ -2595,7 +2624,7 @@ class LoopAgent:
def forget(args: dict[str, Any]) -> tuple[str, bool]:
name = str(args.get("name", "")).strip()
why = " ".join(str(args.get("why", "")).split())
memory.forget(name)
memory.forget(name, scoped(args))
said(f"Forgot: {name}" + (f" ({why})" if why else ""))
return f"forgot {name}", True