feat(memory,spawn): self-documenting .trinity state-tabs + todo delete-on-done discipline

- .trinity sections carry config-sourced rollover/keep/char-cap tabs; @memory owns
  values (render_all_meta_tabs), @spawn resolves placeholders at create via spawn_pusher
- todos confirmed rollover-exempt; vestigial rollover-config entry removed
- prep/memo/startup (Claude+Codex): delete finished todos, don't leave status:done
- @memory README documents the system
- FPLAN-0285, FPLAN-0286
This commit is contained in:
AIOSAI
2026-06-25 03:00:36 -07:00
parent be8f87d6fb
commit fbf102c636
26 changed files with 1090 additions and 39 deletions
+3 -3
View File
@@ -25,14 +25,14 @@ Each memory file plays a distinct role. Update based on what actually changed th
- **`date`** — ISO date/datetime.
- Plus its text field + extras: key_learnings `{number, date, key, value}` · sessions `{number, date, summary, status, tags}` · todos `{number, date, task, priority, status}` · observations `{number, date, note, tags}`.
**When adding:** stamp `number` + `date`, then **prepend** (newest on top). **Don't hand-trim** — rollover archives the oldest *by number* to @memory automatically.
**When adding:** stamp `number` + `date`, then **prepend** (newest on top). **Don't hand-trim** sessions/key_learnings/observations — rollover archives the oldest *by number* to @memory automatically. **Todos are the exception** — rollover never touches them, so you prune done ones by hand (see Reconcile below).
### Reconcile todos — verify against reality, don't trust the label
Stored status drifts: a todo finished in a past session often never gets closed. Before writing the session entry, **audit every open todo against the actual system** — check the real state, not the stored `status`:
- Does the file/dir still exist (or is it gone)? Is the code path in or out? Does the README/doc actually say what the todo claims? Does the audit pass?
- **Close what's verifiably done** → flip to `done` with `verified <date>: <what proved it>`. Fail honestly — close only on evidence, never just to tidy the list.
- **Close what's verifiably done** → note it in the session entry, then **DELETE the todo from the array**. Rollover never trims todos (they're operational — only sessions/key_learnings/observations roll), so done items left as `status: done` pile up and go stale across chats. Fail honestly — remove only on evidence, never just to tidy the list.
- **Re-scope what's partially done** → record which sub-items landed, keep the rest open.
- **Leave deferred / pending-decision todos open** — but confirm they're still real.
@@ -66,7 +66,7 @@ List everything updated. Format:
```
Prep complete:
- local.json: [what was added]
- Todos: [reconciled vs reality — N closed (verified), M re-scoped, K still open]
- Todos: [reconciled vs reality — N done & removed, M re-scoped, K still open]
- observations.json: [updated / skipped]
- Plans: [which ones updated]
- Git: [branch, uncommitted count, suggestion]
+1 -1
View File
@@ -14,7 +14,7 @@ Purpose: Update branch memory files after completing work this session.
Each memory file plays a distinct role. Update based on what actually changed this session.
- **`.trinity/passport.json`** — IDENTITY. Who you are: role, capabilities, principles. Only update if identity genuinely evolved this session. Don't touch it just to touch it.
- **`.trinity/local.json`** — YOUR MEMORY. Session history, key_learnings, and todos[]. Add a session entry for significant work. Add key_learnings for facts you'd need next time. Update todos[] with open items. Trim oldest sessions if over 20.
- **`.trinity/local.json`** — YOUR MEMORY. Add a session entry for significant work; add key_learnings for facts you'd need next time. **Todos: add what you parked, and DELETE every todo you finished this session** — the proof goes in the session entry, not the todo. Rollover never trims todos (they're operational), so done ones you leave behind resurface as "open" next load and you waste time re-confirming them. (Sessions/key_learnings DO auto-roll by number — don't hand-trim those.)
- **`.trinity/observations.json`** — YOUR MEMORY OF THE USER. Collaboration insights, preferences, friction points, flow states. Skip entirely if nothing new about the user this session.
## If Relevant
+2 -2
View File
@@ -18,14 +18,14 @@ Purpose: Update branch memory files after completing work this session.
### Always
- **.trinity/local.json** — Add new session entry to `sessions` if significant work was done. Add new `key_learnings` for facts you'd need next time.
- **.trinity/local.json** — Add a session entry to `sessions` if significant work was done; add `key_learnings` for facts you'd need next time. **Todos: add what you parked, and DELETE every todo you finished this session** — the proof goes in the session entry, not the todo. Rollover never trims todos (they're operational), so done ones you leave behind resurface as "open" next load and you waste time re-confirming them.
- **.trinity/observations.json** — Add notable collaboration insights: breakthrough moments, pattern corrections, flow states, friction points, preference discoveries. Skip if nothing notable this session.
### Entry shape — one rule for all four types
`key_learnings`, `sessions`, `todos` (local.json) and `observations` (observations.json) all share ONE shape: a **list of objects, newest at the top (index 0)**. Every entry carries a **`number`** (monotonic int per type — highest = newest, never reused; new = current max + 1) and a **`date`** (ISO), plus its text field + extras: key_learnings `{number, date, key, value}` · sessions `{number, date, summary, status, tags}` · todos `{number, date, task, priority, status}` · observations `{number, date, note, tags}`.
**When adding:** stamp `number` + `date`, then **prepend** (newest on top). **Don't hand-trim** — rollover archives the oldest *by number* to @memory automatically.
**When adding:** stamp `number` + `date`, then **prepend** (newest on top). **Don't hand-trim** sessions/key_learnings/observations — rollover archives the oldest *by number* to @memory automatically. **Todos are the exception** — rollover never touches them, so you prune done ones by hand (delete finished todos, see above).
### If Relevant
+3 -3
View File
@@ -18,9 +18,9 @@ Purpose: Button up everything at the end of a session — or before a /compact.
- **.trinity/observations.json** — Add collaboration insights if anything notable happened. Skip if nothing new.
- **.trinity/passport.json** — Only update if role/purpose/principles genuinely changed this session.
**Entry shape — one rule for all four types:** `key_learnings`, `sessions`, `todos` (local.json) and `observations` (observations.json) are all **lists, newest at top (index 0)**. Every entry carries a **`number`** (monotonic int per type — highest = newest, never reused; new = current max + 1) and a **`date`** (ISO), plus its text field + extras: key_learnings `{number, date, key, value}` · sessions `{number, date, summary, status, tags}` · todos `{number, date, task, priority, status}` · observations `{number, date, note, tags}`. Stamp `number` + `date` and **prepend**; **don't hand-trim** — rollover archives the oldest *by number* automatically.
**Entry shape — one rule for all four types:** `key_learnings`, `sessions`, `todos` (local.json) and `observations` (observations.json) are all **lists, newest at top (index 0)**. Every entry carries a **`number`** (monotonic int per type — highest = newest, never reused; new = current max + 1) and a **`date`** (ISO), plus its text field + extras: key_learnings `{number, date, key, value}` · sessions `{number, date, summary, status, tags}` · todos `{number, date, task, priority, status}` · observations `{number, date, note, tags}`. Stamp `number` + `date` and **prepend**; **don't hand-trim** sessions/key_learnings/observations — rollover archives the oldest *by number* automatically. **Todos are the exception** — rollover never touches them, so you prune done ones by hand (see Reconcile).
**Reconcile todos — verify against reality, don't trust the label.** Stored status drifts (a todo finished a past session often never got closed). Audit every **open** todo against the actual system: file/dir still there? code path in or out? README says what it claims? audit passes? **Close what's verifiably done** (`done` + `verified <date>: <proof>`), **re-scope** partials, **leave** deferred/pending-decision ones open. Fail honestly — close only on evidence, never to tidy the list. Use `ls`/`find`/`git ls-files`/`grep`/`drone @seedgo audit`, not assumptions.
**Reconcile todos — verify against reality, don't trust the label.** Stored status drifts (a todo finished a past session often never got closed). Audit every **open** todo against the actual system: file/dir still there? code path in or out? README says what it claims? audit passes? **Close what's verifiably done** → note it in the session entry, then **DELETE the todo from the array** (rollover never trims todos — they're operational — so done items left as `status: done` pile up and go stale across chats), **re-scope** partials, **leave** deferred/pending-decision ones open. Fail honestly — remove only on evidence, never to tidy the list. Use `ls`/`find`/`git ls-files`/`grep`/`drone @seedgo audit`, not assumptions.
## 2. Active Plans
@@ -50,7 +50,7 @@ List everything updated. Format:
```
Prep complete:
- local.json: [what was added]
- Todos: [reconciled vs reality — N closed (verified), M re-scoped, K still open]
- Todos: [reconciled vs reality — N done & removed, M re-scoped, K still open]
- observations.json: [updated / skipped]
- Plans: [which ones updated]
- Git: [branch, uncommitted count, suggestion]
+22
View File
@@ -24,6 +24,28 @@ PyPI version — not the changelog header.
loads from the @api secret `telegram/prax_monitor`. Ships a reboot-survivable
`prax-monitor.service` user unit. 937 prax tests green (31 new). (DPLAN-0221)
- **Self-documenting `.trinity` state-tabs** — each memory-file section
(`todos` / `key_learnings` / `sessions` / `observations`) now carries a
config-sourced `⟦ rollover ON/OFF · keep N · ≤chars ⟧` tab rendered directly
above it, so an agent editing a section sees its rollover state and character
cap at the edit point (stops over-limit writes). Values are generated from
`memory.config.json` (single source of truth) via @memory's new
`render_all_meta_tabs()` / `tab_renderer.py`; @memory's `spawn_pusher` carries
the `{{*_META}}` placeholders into @spawn's branch templates, and @spawn
resolves them at create (`build_replacements_dict`, fail-loud on missing keys)
so new branches auto-populate. `refresh_all_tabs` keeps live branches synced;
@memory README documents the system. (FPLAN-0285, FPLAN-0286)
### Changed
- **Todo management — delete-on-done discipline** — `todos[]` are operational
and exempt from rollover (confirmed; the vestigial `todos` entry was removed
from `memory.config.json` rollover defaults). Because rollover never trims
them, finished todos must be **deleted**, not left as `status: done` (which
pile up and resurface as "open" across sessions). `/prep` and `/memo` (Claude
+ Codex) and the `CLAUDE.md` startup protocol now codify: delete each todo when
done (proof → session entry), reconcile on load. (FPLAN-0285)
### Fixed
- **prax-monitor service feedback loop** — the unit wrote its own stdout into
+3 -1
View File
@@ -18,4 +18,6 @@ Use drone commands for all operations. Never raw git, gh, file access, or python
# Memories
Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`.
Update `.trinity/` at natural breakpoints, after milestones, and on `/memo`.
Todos[] don't auto-roll — rollover never trims them. So **delete each todo the moment it's done** (never leave it as `status: done`), and **reconcile on load**: close/remove anything already finished so completed work never resurfaces as "open" and wastes a re-confirm.
+21
View File
@@ -141,6 +141,11 @@
"standard": "handlers",
"reason": "Architectural: imports json.memory_files for metadata updates and monitor.detector for registry reading."
},
{
"file": "apps/handlers/tracking/tab_renderer.py",
"standard": "handlers",
"reason": "Architectural: imports json.memory_files, json.config_loader, and monitor.detector for config-driven tab rendering across all branches."
},
{
"file": "apps/handlers/search/vector_search.py",
"standard": "handlers",
@@ -451,6 +456,12 @@
"standard": "unused_function",
"reason": "is_plan_vectorized is a public API function for external callers."
},
{
"file": "apps/handlers/tracking/tab_renderer.py",
"standard": "unused_function",
"functions": ["render_all_meta_tabs"],
"reason": "Cross-branch public API — called by @spawn's build_replacements_dict to resolve {{*_META}} placeholders at branch creation."
},
{
"file": "apps/modules/templates.py",
"standard": "deep_nesting",
@@ -693,6 +704,16 @@
"standard": "meta",
"reason": "Test file — META block present at lines 1-7; hook false-positive on test file format."
},
{
"file": "tests/test_tab_renderer.py",
"standard": "architecture",
"reason": "Test file — lives in tests/ by design, not in 3-layer apps/ structure."
},
{
"file": "tests/test_tab_renderer.py",
"standard": "documentation",
"reason": "Test file — test functions don't require docstrings."
},
{
"file": "tests/test_handlers.py",
"standard": "architecture",
+37 -6
View File
@@ -64,11 +64,11 @@ memory/
│ ├── storage/ # chroma.py, chroma_subprocess.py
│ ├── symbolic/ # chroma_client, deduplicator, extractor, hook, retriever, storage
│ ├── templates/ # pusher.py, differ.py, spawn_pusher.py
│ ├── tracking/ # line_counter.py
│ ├── tracking/ # line_counter.py, tab_renderer.py
│ ├── vector/ # embedder.py, embed_subprocess.py
│ └── central_writer.py
├── templates/ # LOCAL.template.json, OBSERVATIONS.template.json
├── tests/ # 949 tests (31 test files)
├── tests/ # 978 tests
├── .chroma/ # ChromaDB vector store
└── memory_json/ # Operation logs + custom_config/memory.config.json
```
@@ -95,6 +95,38 @@ All ML operations (fastembed, chromadb) run via subprocess. The main process nev
---
## State-Tabs (`*_meta` keys)
Every `.trinity/local.json` and `.trinity/observations.json` carries inline `*_meta` banner strings that tell the editing agent what rollover rules apply to each section. Example:
```
"sessions_meta": "⟦ rollover ON → oldest archived to @memory · keep 15 · summary ≤300 chars ⟧"
```
**Source of truth:** `memory.config.json` — rollover counts (defaults + per-branch overrides) and entry char limits. Tab strings are *generated*, never hand-written.
**Sections:** `todos_meta` (rollover OFF — operational, never trimmed), `key_learnings_meta`, `sessions_meta`, `observations_meta` (all rollover ON).
### Two value flows
| Scenario | How tabs arrive |
|---|---|
| **Live branches** | `refresh_all_tabs()` walks the registry, renders tabs from config with per-branch overrides, writes them into `.trinity/` files. Wired after rollover, sync-lines, and push-templates. |
| **New branches** | Templates carry `{{TODOS_META}}`, `{{KEY_LEARNINGS_META}}`, `{{SESSIONS_META}}`, `{{OBSERVATIONS_META}}` placeholders. `spawn_pusher` propagates these (unresolved) from memory templates → spawn template sets. At branch creation, @spawn calls `render_all_meta_tabs()` to get rendered defaults and resolves the placeholders. |
### Public API
```python
from aipass.memory.apps.handlers.tracking.tab_renderer import render_all_meta_tabs
tabs = render_all_meta_tabs()
# → {"TODOS_META": "⟦ rollover OFF ...", "KEY_LEARNINGS_META": "⟦ rollover ON ...", ...}
```
Returns defaults (not per-branch overrides) — appropriate for template resolution at branch creation.
---
## Integration Points
**Depends on:**
@@ -117,9 +149,8 @@ All ML operations (fastembed, chromadb) run via subprocess. The main process nev
## Quality
- **Tests:** 949 passed, 0 failures, 0 skips
- **Test files:** 31
- **Seedgo:** 100% — maintained since s12
- **Tests:** 978 passed, 0 failures, 0 skips
- **Seedgo:** 100%
---
@@ -131,7 +162,7 @@ All ML operations (fastembed, chromadb) run via subprocess. The main process nev
---
*Last Updated: 2026-05-16*
*Last Updated: 2026-06-25*
---
[← Back to AIPass](../../../README.md)
@@ -114,7 +114,6 @@ DEFAULT_CONFIG: dict[str, Any] = {
"local": {
"sessions": {"count": 20},
"key_learnings": {"count": 25},
"todos": {"count": 10},
},
"observations": {
"observations": {"count": 25},
@@ -160,6 +160,12 @@ def _merge_metadata(curr_meta: dict, tmpl_meta: dict) -> List[str]:
curr_meta[key] = tmpl_val
changes.append(f"document_metadata.{key}: {old} -> {tmpl_val}")
# _usage
tmpl_usage = tmpl_meta.get("_usage")
if tmpl_usage and curr_meta.get("_usage") != tmpl_usage:
curr_meta["_usage"] = tmpl_usage
changes.append("document_metadata._usage: updated from template")
# Tags
tmpl_tags = tmpl_meta.get("tags", [])
if tmpl_tags and set(curr_meta.get("tags", [])) != set(tmpl_tags):
@@ -0,0 +1,332 @@
# =================== AIPass ====================
# Name: tab_renderer.py
# Description: Config-generated state-tabs for .trinity memory files
# Version: 1.0.0
# Created: 2026-06-25
# Modified: 2026-06-25
# =============================================
"""
Tab Renderer Handler
Generates per-section state-tab strings (e.g. ``⟦ rollover ON ... ⟧``) from
``memory.config.json`` and writes them as ``*_meta`` keys into every branch's
``.trinity/local.json`` and ``.trinity/observations.json``.
Purpose:
Make memory files self-documenting. Each section carries a single-line
banner that tells the editing agent whether rollover is ON/OFF, the keep
count, and the char cap — all derived from config so they never drift.
Independence:
Uses config_loader for config, detector helpers for branch discovery,
and memory_files for safe I/O. No service or module dependencies.
"""
from typing import Any, Dict
from aipass.prax.apps.modules.logger import get_system_logger
from aipass.memory.apps.handlers.json import json_handler
logger = get_system_logger()
_CORRECTED_USAGE_LOCAL = (
"Automated file — add entries within your sections, newest on top. "
"Rollover auto-archives sessions/key_learnings (+ observations.json) to @memory; "
"todos[] are OPERATIONAL and NEVER rolled — prune done ones by hand at /prep. "
"Limits live in @memory’s memory.config.json."
)
_CORRECTED_USAGE_OBS = (
"Automated file — add entries within your sections, newest on top. "
"Rollover auto-archives the oldest observations to @memory. "
"Limits live in @memory’s memory.config.json."
)
# =============================================================================
# KEY ORDERING
# =============================================================================
# Canonical key order for local.json
_LOCAL_KEY_ORDER = [
"document_metadata",
"todos_meta",
"todos",
"key_learnings_meta",
"key_learnings",
"sessions_meta",
"sessions",
]
# Canonical key order for observations.json
_OBSERVATIONS_KEY_ORDER = [
"document_metadata",
"guidelines",
"observations_meta",
"observations",
]
def _reorder_keys(data: Dict[str, Any], key_order: list[str]) -> Dict[str, Any]:
"""Rebuild *data* with keys in *key_order* first, then any remaining keys."""
ordered: Dict[str, Any] = {}
for key in key_order:
if key in data:
ordered[key] = data[key]
# Append any keys not in the canonical order
for key in data:
if key not in ordered:
ordered[key] = data[key]
return ordered
# =============================================================================
# TAB RENDERING
# =============================================================================
def render_tab(
section_name: str,
rollover_cfg: dict,
entry_limits_cfg: dict,
branch_name: str,
) -> str:
"""Generate the state-tab string for a section.
Args:
section_name: One of 'key_learnings', 'sessions', 'observations', 'todos'.
rollover_cfg: The ``rollover`` section from memory.config.json.
entry_limits_cfg: The ``entry_limits`` section from memory.config.json.
branch_name: Branch name (lowercase) for per-branch overrides.
Returns:
The rendered tab string (e.g. ``⟦ rollover ON ... ⟧``).
"""
# --- Resolve entry-limits for this section --------------------------------
entry_types = entry_limits_cfg.get("entry_types", {})
section_limits = entry_types.get(section_name, {})
max_chars = section_limits.get("max_chars", 300)
field = section_limits.get("field", "value")
# --- Todos are special: rollover OFF, static shape ------------------------
if section_name == "todos":
return (
f"⟦ rollover OFF — operational, never trimmed · "
f"cap ~10 entries · task ≤{max_chars} chars ⟧ "
f"RULE: DELETE each todo when done (never leave status:done) "
f"+ reconcile on load; proof goes in the session entry, "
f"not the todo. Add freely — BAU."
)
# --- Rollover sections: look up count -------------------------------------
per_branch = rollover_cfg.get("per_branch", {})
defaults = rollover_cfg.get("defaults", {})
branch_cfg = per_branch.get(branch_name, defaults)
# Determine which file-level block to read
if section_name == "observations":
file_block = branch_cfg.get("observations", {})
else:
file_block = branch_cfg.get("local", {})
section_cfg = file_block.get(section_name, {})
count = section_cfg.get("count", 15)
return f"⟦ rollover ON → oldest archived to @memory · keep {count} · {field} ≤{max_chars} chars ⟧"
# =============================================================================
# PER-FILE TAB WRITERS
# =============================================================================
def _refresh_local(branch_name, local_path, rollover_cfg, entry_limits_cfg):
"""Inject *_meta tabs into a branch's local.json. Returns (ok, error_msg)."""
from aipass.memory.apps.handlers.json.memory_files import (
read_memory_file_data,
write_memory_file_simple,
)
data = read_memory_file_data(local_path)
if data is None:
return False, None # file unreadable, skip silently
meta = data.get("document_metadata", {})
if meta.get("_usage") != _CORRECTED_USAGE_LOCAL:
meta["_usage"] = _CORRECTED_USAGE_LOCAL
data["todos_meta"] = render_tab(
"todos",
rollover_cfg,
entry_limits_cfg,
branch_name,
)
data["key_learnings_meta"] = render_tab(
"key_learnings",
rollover_cfg,
entry_limits_cfg,
branch_name,
)
data["sessions_meta"] = render_tab(
"sessions",
rollover_cfg,
entry_limits_cfg,
branch_name,
)
data = _reorder_keys(data, _LOCAL_KEY_ORDER)
if write_memory_file_simple(local_path, data):
return True, None
return False, f"{branch_name}/local.json: write failed"
def _refresh_observations(branch_name, obs_path, rollover_cfg, entry_limits_cfg):
"""Inject observations_meta tab into a branch's observations.json. Returns (ok, error_msg)."""
from aipass.memory.apps.handlers.json.memory_files import (
read_memory_file_data,
write_memory_file_simple,
)
data = read_memory_file_data(obs_path)
if data is None:
return False, None # file unreadable, skip silently
meta = data.get("document_metadata", {})
if meta.get("_usage") != _CORRECTED_USAGE_OBS:
meta["_usage"] = _CORRECTED_USAGE_OBS
data["observations_meta"] = render_tab(
"observations",
rollover_cfg,
entry_limits_cfg,
branch_name,
)
data = _reorder_keys(data, _OBSERVATIONS_KEY_ORDER)
if write_memory_file_simple(obs_path, data):
return True, None
return False, f"{branch_name}/observations.json: write failed"
# =============================================================================
# REFRESH ALL BRANCHES
# =============================================================================
def refresh_all_tabs() -> dict:
"""Render and write state-tabs to all branch .trinity files.
Walks the registry, reads each branch's memory files, computes tab
strings from config, injects them as ``*_meta`` keys, and writes back
with correct key ordering.
Returns:
Dict with success status and counts.
"""
from aipass.memory.apps.handlers.json.config_loader import (
load as load_config,
)
from aipass.memory.apps.handlers.monitor.detector import (
_read_registry,
_get_memory_file_path,
)
config = load_config()
rollover_cfg = config.get("rollover", {})
entry_limits_cfg = config.get("entry_limits", {})
branches = _read_registry()
if not branches:
return {
"success": True,
"updated": 0,
"skipped": 0,
"message": "No branches in registry",
}
updated = 0
skipped = 0
errors: list[str] = []
for branch in branches:
branch_name = branch.get("name", "UNKNOWN").lower()
for mem_type in ("local", "observations"):
u, s, e = _refresh_one_file(
branch,
branch_name,
mem_type,
rollover_cfg,
entry_limits_cfg,
_get_memory_file_path,
)
updated += u
skipped += s
errors.extend(e)
json_handler.log_operation(
"refresh_all_tabs",
{"updated": updated, "skipped": skipped, "errors": len(errors)},
module_name="tab_renderer",
)
logger.info(
f"[tab_renderer] Refreshed tabs: {updated} updated, {skipped} skipped, {len(errors)} errors",
)
return {
"success": True,
"updated": updated,
"skipped": skipped,
"errors": errors,
}
def render_all_meta_tabs() -> dict[str, str]:
"""Render all four *_meta tab strings from memory.config.json defaults.
Public API for @spawn (and any other consumer) to resolve ``{{*_META}}``
placeholders at branch-creation time.
Returns:
Dict with keys TODOS_META, KEY_LEARNINGS_META, SESSIONS_META,
OBSERVATIONS_META — each a rendered state-tab string.
"""
from aipass.memory.apps.handlers.json.config_loader import (
load as load_config,
)
config = load_config()
rollover_cfg = config.get("rollover", {})
entry_limits_cfg = config.get("entry_limits", {})
_default = "__template_default__"
return {
"TODOS_META": render_tab("todos", rollover_cfg, entry_limits_cfg, _default),
"KEY_LEARNINGS_META": render_tab("key_learnings", rollover_cfg, entry_limits_cfg, _default),
"SESSIONS_META": render_tab("sessions", rollover_cfg, entry_limits_cfg, _default),
"OBSERVATIONS_META": render_tab("observations", rollover_cfg, entry_limits_cfg, _default),
}
def _refresh_one_file(branch, branch_name, mem_type, rollover_cfg, entry_limits_cfg, get_path_fn):
"""Refresh tabs for a single memory file. Returns (updated, skipped, errors)."""
file_path = get_path_fn(branch, mem_type)
if file_path is None:
return 0, 1, []
refresher = _refresh_local if mem_type == "local" else _refresh_observations
try:
ok, err = refresher(
branch_name,
file_path,
rollover_cfg,
entry_limits_cfg,
)
except Exception as e:
logger.warning(f"[tab_renderer] {branch_name}/{mem_type}.json: {e}")
return 0, 0, [f"{branch_name}/{mem_type}.json: {e}"]
if ok:
return 1, 0, []
if err:
return 0, 0, [err]
return 0, 1, []
@@ -236,6 +236,15 @@ def run_rollover() -> bool:
error(f"{fail['trigger']} - {fail['stage']}: {fail['error']}")
json_handler.log_operation("rollover_execute", {"triggers": triggers_count, "success_count": success_count})
# Refresh state-tabs after rollover (counts may have changed)
try:
from aipass.memory.apps.handlers.tracking.tab_renderer import refresh_all_tabs
refresh_all_tabs()
except Exception as e:
logger.warning(f"[rollover] Tab refresh failed: {e}")
return success_count > 0
@@ -325,6 +334,14 @@ def sync_line_counts() -> None:
else:
error("Failed to sync line counts")
# Refresh state-tabs after line count sync
try:
from aipass.memory.apps.handlers.tracking.tab_renderer import refresh_all_tabs
refresh_all_tabs()
except Exception as e:
logger.warning(f"[rollover] Tab refresh failed: {e}")
console.print()
@@ -119,6 +119,14 @@ def handle_command(command: str, args: List[str]) -> bool:
except Exception as e:
error(f"Spawn template push crashed: {e}")
logger.error(f"[templates] spawn push crashed: {e}")
# Refresh state-tabs after template push
if not dry_run:
try:
from aipass.memory.apps.handlers.tracking.tab_renderer import refresh_all_tabs
refresh_all_tabs()
except Exception as e:
logger.warning(f"[templates] Tab refresh failed: {e}")
return True
if sub == "diff-templates":
@@ -162,6 +170,14 @@ def handle_command(command: str, args: List[str]) -> bool:
except Exception as e:
error(f"Spawn template push crashed: {e}")
logger.error(f"[templates] spawn push crashed: {e}")
# Refresh state-tabs after template push
if not dry_run:
try:
from aipass.memory.apps.handlers.tracking.tab_renderer import refresh_all_tabs
refresh_all_tabs()
except Exception as e:
logger.warning(f"[templates] Tab refresh failed: {e}")
return True
elif command == "diff-templates":
@@ -1,7 +1,11 @@
{
"last_push": "2026-06-07 21:40:48",
"last_push": "2026-06-25 01:55:01",
"last_push_branches": [
"COMMONS",
"DAEMON",
"HOOKS",
"SKILLS",
"ai_mail",
"aipass",
"api",
"cli",
@@ -12,14 +12,17 @@
"work_log",
"{{BRANCHNAME}}"
],
"_usage": "Automated file — add entries within your sections; rollover trims automatically. Limits live in @memory's memory.config.json.",
"_usage": "Automated file — add entries within your sections, newest on top. Rollover auto-archives sessions/key_learnings (+ observations.json) to @memory; todos[] are OPERATIONAL and NEVER rolled — prune done ones by hand at /prep. Limits live in @memory's memory.config.json.",
"status": {
"health": "healthy",
"last_health_check": "{{DATE}}"
}
},
"key_learnings": [],
"todos_meta": "{{TODOS_META}}",
"todos": [],
"key_learnings_meta": "{{KEY_LEARNINGS_META}}",
"key_learnings": [],
"sessions_meta": "{{SESSIONS_META}}",
"sessions": [
{
"number": 1,
@@ -12,7 +12,7 @@
"patterns",
"{{BRANCHNAME}}"
],
"_usage": "Automated file — add entries within your sections; rollover trims automatically. Limits live in @memory's memory.config.json.",
"_usage": "Automated file — add entries within your sections, newest on top. Rollover auto-archives sessions/key_learnings (+ observations.json) to @memory; todos[] are OPERATIONAL and NEVER rolled — prune done ones by hand at /prep. Limits live in @memory's memory.config.json.",
"status": {
"health": "healthy",
"last_health_check": "{{DATE}}"
@@ -22,6 +22,7 @@
"purpose": "Capture collaboration patterns and experiential insights over time",
"chronological_order": "Newest entries at TOP, oldest at BOTTOM - NEVER reorder"
},
"observations_meta": "{{OBSERVATIONS_META}}",
"observations": [
{
"number": 1,
@@ -0,0 +1,571 @@
# =================== AIPass ====================
# Name: test_tab_renderer.py
# Description: Tests for tab_renderer handler (FPLAN-0285)
# Version: 1.0.0
# Created: 2026-06-25
# Modified: 2026-06-25
# =============================================
"""
Tests for the tab_renderer handler.
Covers:
1. render_tab() — correct strings for each section type.
2. render_tab() — per-branch overrides from config.
3. render_tab() — fallback to defaults when branch not in per_branch.
4. _reorder_keys() — canonical key ordering.
5. refresh_all_tabs() — reads config and writes tabs (mocked I/O).
6. Key ordering verification after tab insertion.
"""
import importlib
import json
import sys
from pathlib import Path
from unittest.mock import patch
import pytest
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def _fresh_tab_renderer(monkeypatch):
"""Drop cached module so each test gets a fresh import."""
for mod_name in list(sys.modules):
if "tab_renderer" in mod_name:
sys.modules.pop(mod_name, None)
# Also clear json sub-modules that conftest replaces with MagicMock
sys.modules.pop("aipass.memory.apps.handlers.json", None)
sys.modules.pop("aipass.memory.apps.handlers.json.json_handler", None)
yield
def _get_module():
"""Import and return the tab_renderer module."""
return importlib.import_module(
"aipass.memory.apps.handlers.tracking.tab_renderer",
)
# ---------------------------------------------------------------------------
# Shared config fixtures
# ---------------------------------------------------------------------------
SAMPLE_ROLLOVER_CFG = {
"defaults": {
"local": {
"sessions": {"count": 15},
"key_learnings": {"count": 15},
},
"observations": {
"observations": {"count": 15},
},
},
"per_branch": {
"devpulse": {
"local": {
"sessions": {"count": 20},
"key_learnings": {"count": 25},
},
"observations": {
"observations": {"count": 30},
},
},
},
}
SAMPLE_ENTRY_LIMITS_CFG = {
"entry_types": {
"key_learnings": {"field": "value", "max_chars": 200},
"sessions": {"field": "summary", "max_chars": 300},
"todos": {"field": "task", "max_chars": 150},
"observations": {"field": "note", "max_chars": 300},
},
}
# ===========================================================================
# 1. render_tab — key_learnings (rollover ON)
# ===========================================================================
class TestRenderTabKeyLearnings:
def test_default_branch(self):
mod = _get_module()
tab = mod.render_tab(
"key_learnings",
SAMPLE_ROLLOVER_CFG,
SAMPLE_ENTRY_LIMITS_CFG,
"memory",
)
assert tab.startswith("⟦")
assert tab.endswith("⟧")
assert "rollover ON" in tab
assert "keep 15" in tab
assert "value ≤20" in tab # ≤200
def test_per_branch_override(self):
mod = _get_module()
tab = mod.render_tab(
"key_learnings",
SAMPLE_ROLLOVER_CFG,
SAMPLE_ENTRY_LIMITS_CFG,
"devpulse",
)
assert "keep 25" in tab
# ===========================================================================
# 2. render_tab — sessions (rollover ON)
# ===========================================================================
class TestRenderTabSessions:
def test_sessions_default(self):
mod = _get_module()
tab = mod.render_tab(
"sessions",
SAMPLE_ROLLOVER_CFG,
SAMPLE_ENTRY_LIMITS_CFG,
"memory",
)
assert "rollover ON" in tab
assert "keep 15" in tab
assert "summary" in tab
assert "≤30" in tab # ≤300
def test_sessions_per_branch(self):
mod = _get_module()
tab = mod.render_tab(
"sessions",
SAMPLE_ROLLOVER_CFG,
SAMPLE_ENTRY_LIMITS_CFG,
"devpulse",
)
assert "keep 20" in tab
# ===========================================================================
# 3. render_tab — observations (rollover ON)
# ===========================================================================
class TestRenderTabObservations:
def test_observations_default(self):
mod = _get_module()
tab = mod.render_tab(
"observations",
SAMPLE_ROLLOVER_CFG,
SAMPLE_ENTRY_LIMITS_CFG,
"memory",
)
assert "rollover ON" in tab
assert "keep 15" in tab
assert "note" in tab
def test_observations_per_branch(self):
mod = _get_module()
tab = mod.render_tab(
"observations",
SAMPLE_ROLLOVER_CFG,
SAMPLE_ENTRY_LIMITS_CFG,
"devpulse",
)
assert "keep 30" in tab
# ===========================================================================
# 4. render_tab — todos (rollover OFF, static shape)
# ===========================================================================
class TestRenderTabTodos:
def test_todos_static(self):
mod = _get_module()
tab = mod.render_tab(
"todos",
SAMPLE_ROLLOVER_CFG,
SAMPLE_ENTRY_LIMITS_CFG,
"memory",
)
assert "rollover OFF" in tab
assert "cap ~10 entries" in tab
assert "task ≤15" in tab # ≤150
assert "RULE: DELETE" in tab
assert "BAU" in tab
def test_todos_ignores_per_branch_rollover(self):
"""Todos are always rollover OFF regardless of per_branch config."""
mod = _get_module()
tab = mod.render_tab(
"todos",
SAMPLE_ROLLOVER_CFG,
SAMPLE_ENTRY_LIMITS_CFG,
"devpulse",
)
assert "rollover OFF" in tab
# ===========================================================================
# 5. _reorder_keys — canonical key ordering
# ===========================================================================
class TestReorderKeys:
def test_local_key_order(self):
mod = _get_module()
data = {
"sessions": [],
"document_metadata": {},
"todos": [],
"key_learnings": [],
"extra_field": "preserved",
}
ordered = mod._reorder_keys(data, mod._LOCAL_KEY_ORDER)
keys = list(ordered.keys())
assert keys[0] == "document_metadata"
# todos before key_learnings before sessions
assert keys.index("todos") < keys.index("key_learnings")
assert keys.index("key_learnings") < keys.index("sessions")
# extra_field at the end
assert keys[-1] == "extra_field"
def test_observations_key_order(self):
mod = _get_module()
data = {
"observations": [],
"document_metadata": {},
"guidelines": {},
"observations_meta": "tab",
}
ordered = mod._reorder_keys(data, mod._OBSERVATIONS_KEY_ORDER)
keys = list(ordered.keys())
assert keys == [
"document_metadata",
"guidelines",
"observations_meta",
"observations",
]
def test_meta_before_array(self):
"""Meta key must appear immediately before its corresponding array."""
mod = _get_module()
data = {
"document_metadata": {},
"todos": [],
"todos_meta": "tab-todos",
"key_learnings": [],
"key_learnings_meta": "tab-kl",
"sessions": [],
"sessions_meta": "tab-sessions",
}
ordered = mod._reorder_keys(data, mod._LOCAL_KEY_ORDER)
keys = list(ordered.keys())
# Each *_meta must be immediately before its array
assert keys.index("todos_meta") + 1 == keys.index("todos")
assert keys.index("key_learnings_meta") + 1 == keys.index(
"key_learnings",
)
assert keys.index("sessions_meta") + 1 == keys.index("sessions")
# ===========================================================================
# 6. refresh_all_tabs — integration with mocked I/O
# ===========================================================================
class TestRefreshAllTabs:
def _make_local_data(self):
return {
"document_metadata": {"document_type": "session_history"},
"todos": [{"task": "test"}],
"key_learnings": [],
"sessions": [],
}
def _make_obs_data(self):
return {
"document_metadata": {
"document_type": "collaboration_patterns",
},
"guidelines": {},
"observations": [],
}
def test_writes_tabs_to_files(self, tmp_path):
"""refresh_all_tabs reads config, walks branches, writes tabs."""
mod = _get_module()
# Set up branch dir with .trinity files
branch_dir = tmp_path / "src" / "aipass" / "test_branch"
trinity = branch_dir / ".trinity"
trinity.mkdir(parents=True)
local_path = trinity / "local.json"
obs_path = trinity / "observations.json"
local_path.write_text(
json.dumps(self._make_local_data(), indent=2),
encoding="utf-8",
)
obs_path.write_text(
json.dumps(self._make_obs_data(), indent=2),
encoding="utf-8",
)
# Mock registry to return our test branch
mock_branches = [
{"name": "test_branch", "path": str(branch_dir)},
]
def mock_get_path(branch, mem_type):
p = Path(branch["path"]) / ".trinity" / f"{mem_type}.json"
return p if p.exists() else None
mock_config = {
"rollover": SAMPLE_ROLLOVER_CFG,
"entry_limits": SAMPLE_ENTRY_LIMITS_CFG,
}
with (
patch(
"aipass.memory.apps.handlers.json.config_loader.load",
return_value=mock_config,
),
patch(
"aipass.memory.apps.handlers.monitor.detector._read_registry",
return_value=mock_branches,
),
patch(
"aipass.memory.apps.handlers.monitor.detector._get_memory_file_path",
side_effect=mock_get_path,
),
):
result = mod.refresh_all_tabs()
assert result["success"] is True
assert result["updated"] == 2 # local + observations
# Verify local.json has tabs
local_data = json.loads(local_path.read_text(encoding="utf-8"))
assert "todos_meta" in local_data
assert "key_learnings_meta" in local_data
assert "sessions_meta" in local_data
assert "rollover OFF" in local_data["todos_meta"]
assert "rollover ON" in local_data["key_learnings_meta"]
assert "rollover ON" in local_data["sessions_meta"]
# Verify observations.json has tab
obs_data = json.loads(obs_path.read_text(encoding="utf-8"))
assert "observations_meta" in obs_data
assert "rollover ON" in obs_data["observations_meta"]
def test_key_order_after_refresh(self, tmp_path):
"""After refresh, keys are in canonical order."""
mod = _get_module()
branch_dir = tmp_path / "src" / "aipass" / "ordered_branch"
trinity = branch_dir / ".trinity"
trinity.mkdir(parents=True)
local_path = trinity / "local.json"
local_path.write_text(
json.dumps(self._make_local_data(), indent=2),
encoding="utf-8",
)
obs_path = trinity / "observations.json"
obs_path.write_text(
json.dumps(self._make_obs_data(), indent=2),
encoding="utf-8",
)
mock_branches = [
{"name": "ordered_branch", "path": str(branch_dir)},
]
def mock_get_path(branch, mem_type):
p = Path(branch["path"]) / ".trinity" / f"{mem_type}.json"
return p if p.exists() else None
mock_config = {
"rollover": SAMPLE_ROLLOVER_CFG,
"entry_limits": SAMPLE_ENTRY_LIMITS_CFG,
}
with (
patch(
"aipass.memory.apps.handlers.json.config_loader.load",
return_value=mock_config,
),
patch(
"aipass.memory.apps.handlers.monitor.detector._read_registry",
return_value=mock_branches,
),
patch(
"aipass.memory.apps.handlers.monitor.detector._get_memory_file_path",
side_effect=mock_get_path,
),
):
mod.refresh_all_tabs()
local_data = json.loads(local_path.read_text(encoding="utf-8"))
keys = list(local_data.keys())
expected_prefix = [
"document_metadata",
"todos_meta",
"todos",
"key_learnings_meta",
"key_learnings",
"sessions_meta",
"sessions",
]
assert keys[: len(expected_prefix)] == expected_prefix
def test_empty_registry(self):
"""refresh_all_tabs returns early if no branches in registry."""
mod = _get_module()
mock_config = {
"rollover": SAMPLE_ROLLOVER_CFG,
"entry_limits": SAMPLE_ENTRY_LIMITS_CFG,
}
with (
patch(
"aipass.memory.apps.handlers.json.config_loader.load",
return_value=mock_config,
),
patch(
"aipass.memory.apps.handlers.monitor.detector._read_registry",
return_value=[],
),
):
result = mod.refresh_all_tabs()
assert result["success"] is True
assert result["updated"] == 0
assert "No branches" in result.get("message", "")
def test_no_templates_updated_key(self, tmp_path):
"""refresh_all_tabs result dict has no templates_updated key (literal-baking removed)."""
mod = _get_module()
mock_config = {
"rollover": SAMPLE_ROLLOVER_CFG,
"entry_limits": SAMPLE_ENTRY_LIMITS_CFG,
}
with (
patch(
"aipass.memory.apps.handlers.json.config_loader.load",
return_value=mock_config,
),
patch(
"aipass.memory.apps.handlers.monitor.detector._read_registry",
return_value=[],
),
):
result = mod.refresh_all_tabs()
assert "templates_updated" not in result
def test_missing_file_skipped(self, tmp_path):
"""Branch with missing .trinity files is skipped, not errored."""
mod = _get_module()
branch_dir = tmp_path / "src" / "aipass" / "empty_branch"
branch_dir.mkdir(parents=True)
# No .trinity directory at all
mock_branches = [
{"name": "empty_branch", "path": str(branch_dir)},
]
def mock_get_path(branch, mem_type):
p = Path(branch["path"]) / ".trinity" / f"{mem_type}.json"
return p if p.exists() else None
mock_config = {
"rollover": SAMPLE_ROLLOVER_CFG,
"entry_limits": SAMPLE_ENTRY_LIMITS_CFG,
}
with (
patch(
"aipass.memory.apps.handlers.json.config_loader.load",
return_value=mock_config,
),
patch(
"aipass.memory.apps.handlers.monitor.detector._read_registry",
return_value=mock_branches,
),
patch(
"aipass.memory.apps.handlers.monitor.detector._get_memory_file_path",
side_effect=mock_get_path,
),
):
result = mod.refresh_all_tabs()
assert result["success"] is True
assert result["skipped"] == 2 # local + observations
assert result["updated"] == 0
# ===========================================================================
# 8. render_all_meta_tabs — public API for spawn
# ===========================================================================
class TestRenderAllMetaTabs:
def test_returns_four_keys(self):
mod = _get_module()
mock_config = {
"rollover": SAMPLE_ROLLOVER_CFG,
"entry_limits": SAMPLE_ENTRY_LIMITS_CFG,
}
with patch(
"aipass.memory.apps.handlers.json.config_loader.load",
return_value=mock_config,
):
tabs = mod.render_all_meta_tabs()
assert set(tabs.keys()) == {
"TODOS_META",
"KEY_LEARNINGS_META",
"SESSIONS_META",
"OBSERVATIONS_META",
}
def test_values_are_rendered_strings(self):
mod = _get_module()
mock_config = {
"rollover": SAMPLE_ROLLOVER_CFG,
"entry_limits": SAMPLE_ENTRY_LIMITS_CFG,
}
with patch(
"aipass.memory.apps.handlers.json.config_loader.load",
return_value=mock_config,
):
tabs = mod.render_all_meta_tabs()
assert "rollover OFF" in tabs["TODOS_META"]
assert "rollover ON" in tabs["KEY_LEARNINGS_META"]
assert "rollover ON" in tabs["SESSIONS_META"]
assert "rollover ON" in tabs["OBSERVATIONS_META"]
assert "{{" not in tabs["TODOS_META"]
def test_uses_defaults_not_per_branch(self):
mod = _get_module()
mock_config = {
"rollover": SAMPLE_ROLLOVER_CFG,
"entry_limits": SAMPLE_ENTRY_LIMITS_CFG,
}
with patch(
"aipass.memory.apps.handlers.json.config_loader.load",
return_value=mock_config,
):
tabs = mod.render_all_meta_tabs()
assert "keep 15" in tabs["KEY_LEARNINGS_META"]
assert "keep 15" in tabs["SESSIONS_META"]
+3 -9
View File
@@ -73,25 +73,19 @@
],
"document_metadata": {
"version": "2.0.0",
"schema_version": "2.0.0",
"schema_version": "3.0.0",
"document_type": "session_history",
"tags": [
"session_tracking",
"work_log",
"prax"
],
"limits": {
"max_sessions": 20,
"max_key_learnings": 25,
"session_summary_max_chars": 150,
"learning_value_max_chars": 200,
"note": "DO NOT trim, prune, or delete entries. Rollover to @memory handles overflow automatically. Just add new entries."
},
"status": {
"health": "healthy",
"last_health_check": "2026-04-22",
"current_lines": 0
}
},
"key_learnings": {}
"key_learnings": {},
"todos": []
}
+5
View File
@@ -231,6 +231,11 @@
"file": "apps/handlers/meta_ops.py",
"standard": "unused_function",
"reason": "load_branch_meta() no longer called by P1 engine but has tests and may be reused by sync-registry or P2 shared library."
},
{
"file": "apps/modules/core.py",
"standard": "encapsulation",
"reason": "Local import of render_all_meta_tabs from memory.apps.handlers.tracking.tab_renderer — @memory has no module entry point for this API. Cross-branch contract per FPLAN-0286."
}
],
"notes": {
@@ -71,6 +71,10 @@ def build_replacements_dict(target_dir, branch_name, **overrides):
"PROVIDES_TO": "",
}
meta_tabs = overrides.get("meta_tabs")
if meta_tabs:
replacements.update(meta_tabs)
return replacements
+15
View File
@@ -59,6 +59,19 @@ from aipass.spawn.apps.handlers.json import json_handler
# Default template location (relative to spawn package root)
DEFAULT_TEMPLATE = Path(__file__).parents[2] / "templates" / "builder"
_META_TAB_KEYS = {"TODOS_META", "KEY_LEARNINGS_META", "SESSIONS_META", "OBSERVATIONS_META"}
def _load_meta_tabs():
"""Load memory meta-tab values from @memory's renderer. Raises on failure."""
from aipass.memory.apps.handlers.tracking.tab_renderer import render_all_meta_tabs
tabs = render_all_meta_tabs()
missing = _META_TAB_KEYS - set(tabs or {})
if missing:
raise RuntimeError(f"render_all_meta_tabs() missing keys: {sorted(missing)}")
return tabs
def print_introspection():
"""Display module introspection info."""
@@ -219,6 +232,7 @@ def _spawn_agent(
citizen_number = get_next_citizen_number(reg_path)
# Build placeholder replacements
meta_tabs = _load_meta_tabs()
replacements = build_replacements_dict(
target,
folder_name,
@@ -227,6 +241,7 @@ def _spawn_agent(
purpose=purpose or "New agent - purpose TBD",
profile=detected_profile,
citizen_number=citizen_number,
meta_tabs=meta_tabs,
)
# Step 1: Copy template with placeholder replacement in content
@@ -12,14 +12,17 @@
"work_log",
"{{BRANCHNAME}}"
],
"_usage": "Automated file — add entries within your sections; rollover trims automatically. Limits live in @memory's memory.config.json.",
"_usage": "Automated file — add entries within your sections, newest on top. Rollover auto-archives sessions/key_learnings (+ observations.json) to @memory; todos[] are OPERATIONAL and NEVER rolled — prune done ones by hand at /prep. Limits live in @memory's memory.config.json.",
"status": {
"health": "healthy",
"last_health_check": "{{DATE}}"
}
},
"key_learnings": [],
"todos_meta": "{{TODOS_META}}",
"todos": [],
"key_learnings_meta": "{{KEY_LEARNINGS_META}}",
"key_learnings": [],
"sessions_meta": "{{SESSIONS_META}}",
"sessions": [
{
"number": 1,
@@ -12,7 +12,7 @@
"patterns",
"{{BRANCHNAME}}"
],
"_usage": "Automated file — add entries within your sections; rollover trims automatically. Limits live in @memory's memory.config.json.",
"_usage": "Automated file — add entries within your sections, newest on top. Rollover auto-archives sessions/key_learnings (+ observations.json) to @memory; todos[] are OPERATIONAL and NEVER rolled — prune done ones by hand at /prep. Limits live in @memory's memory.config.json.",
"status": {
"health": "healthy",
"last_health_check": "{{DATE}}"
@@ -22,6 +22,7 @@
"purpose": "Capture collaboration patterns and experiential insights over time",
"chronological_order": "Newest entries at TOP, oldest at BOTTOM - NEVER reorder"
},
"observations_meta": "{{OBSERVATIONS_META}}",
"observations": [
{
"number": 1,
@@ -184,13 +184,13 @@
"path": ".trinity/README.md"
},
"f012": {
"content_hash": "8f8a98e42d92",
"content_hash": "a68a905541ee",
"has_branch_placeholder": false,
"name": "local.json",
"path": ".trinity/local.json"
},
"f013": {
"content_hash": "62160dfa243c",
"content_hash": "e51a865855a6",
"has_branch_placeholder": false,
"name": "observations.json",
"path": ".trinity/observations.json"
@@ -384,7 +384,7 @@
},
"metadata": {
"description": "Template file tracking registry for ID-based updates",
"last_updated": "2026-06-11",
"last_updated": "2026-06-25",
"version": "1.0.0"
}
}
@@ -12,14 +12,17 @@
"work_log",
"{{BRANCHNAME}}"
],
"_usage": "Automated file — add entries within your sections; rollover trims automatically. Limits live in @memory's memory.config.json.",
"_usage": "Automated file — add entries within your sections, newest on top. Rollover auto-archives sessions/key_learnings (+ observations.json) to @memory; todos[] are OPERATIONAL and NEVER rolled — prune done ones by hand at /prep. Limits live in @memory's memory.config.json.",
"status": {
"health": "healthy",
"last_health_check": "{{DATE}}"
}
},
"key_learnings": [],
"todos_meta": "{{TODOS_META}}",
"todos": [],
"key_learnings_meta": "{{KEY_LEARNINGS_META}}",
"key_learnings": [],
"sessions_meta": "{{SESSIONS_META}}",
"sessions": [
{
"number": 1,
@@ -12,7 +12,7 @@
"patterns",
"{{BRANCHNAME}}"
],
"_usage": "Automated file — add entries within your sections; rollover trims automatically. Limits live in @memory's memory.config.json.",
"_usage": "Automated file — add entries within your sections, newest on top. Rollover auto-archives sessions/key_learnings (+ observations.json) to @memory; todos[] are OPERATIONAL and NEVER rolled — prune done ones by hand at /prep. Limits live in @memory's memory.config.json.",
"status": {
"health": "healthy",
"last_health_check": "{{DATE}}"
@@ -22,6 +22,7 @@
"purpose": "Capture collaboration patterns and experiential insights over time",
"chronological_order": "Newest entries at TOP, oldest at BOTTOM - NEVER reorder"
},
"observations_meta": "{{OBSERVATIONS_META}}",
"observations": [
{
"number": 1,