2026-04-13 16:09:12 -04:00
|
|
|
|
"""AtoCore Wiki — navigable HTML pages from structured data.
|
|
|
|
|
|
|
|
|
|
|
|
A lightweight wiki served directly from the AtoCore API. Every page is
|
|
|
|
|
|
generated on-demand from the database so it's always current. Source of
|
|
|
|
|
|
truth is the database — the wiki is a derived view.
|
|
|
|
|
|
|
|
|
|
|
|
Routes:
|
|
|
|
|
|
/wiki Homepage with project list + search
|
|
|
|
|
|
/wiki/projects/{name} Full project overview
|
|
|
|
|
|
/wiki/entities/{id} Entity detail with relationships
|
|
|
|
|
|
/wiki/search?q=... Search entities, memories, state
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import markdown as md
|
|
|
|
|
|
|
|
|
|
|
|
from atocore.context.project_state import get_state
|
|
|
|
|
|
from atocore.engineering.service import (
|
|
|
|
|
|
get_entities,
|
|
|
|
|
|
get_entity,
|
|
|
|
|
|
get_entity_with_context,
|
|
|
|
|
|
get_relationships,
|
|
|
|
|
|
)
|
|
|
|
|
|
from atocore.memory.service import get_memories
|
feat(entities): inbox + cross-project (project="") support (Issue C)
Makes `inbox` a reserved pseudo-project and `project=""` a first-class
cross-project bucket. Unblocks AKC capturing pre-project leads/quotes
and cross-project facts (materials, vendors) that don't fit a single
registered project.
- projects/registry.py: INBOX_PROJECT/GLOBAL_PROJECT constants,
is_reserved_project(), register/update guards, resolve_project_name
passthrough for "inbox"
- engineering/service.py: get_entities scoping rules (inbox-only,
global-only, real+global default, scope_only=true strict).
promote_entity accepts target_project to retarget on promote
- api/routes.py: GET /entities gains scope_only; POST /entities accepts
project=null as ""; POST /entities/{id}/promote accepts
{target_project, note}
- engineering/wiki.py: homepage shows "Inbox & Global" cards with live
counts linking to scoped lists
- tests/test_inbox_crossproject.py: 15 tests (reserved enforcement,
scoping rules, API shape, promote retargeting)
- DEV-LEDGER.md: session log, test_count 463 -> 478
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 20:17:32 -04:00
|
|
|
|
from atocore.projects.registry import (
|
|
|
|
|
|
GLOBAL_PROJECT,
|
|
|
|
|
|
INBOX_PROJECT,
|
|
|
|
|
|
load_project_registry,
|
|
|
|
|
|
)
|
2026-04-13 16:09:12 -04:00
|
|
|
|
|
|
|
|
|
|
|
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav)
Closes three gaps the user surfaced: (1) OpenClaw agents run blind
without AtoCore context, (2) mobile/desktop chats can't be captured
at all, (3) wiki UI hadn't kept up with backend capabilities.
Phase 7I — OpenClaw two-way bridge
- Plugin now calls /context/build on before_agent_start and prepends
the context pack to event.prompt, so whatever LLM runs underneath
(sonnet, opus, codex, local model) answers grounded in AtoCore
knowledge. Captured prompt stays the user's original text; fail-open
with a 5s timeout. Config-gated via injectContext flag.
- Plugin version 0.0.0 → 0.2.0; README rewritten.
UI refresh
- /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile
/ ChatGPT / other. Goes through normal /interactions pipeline with
client="claude-desktop|claude-web|claude-mobile|chatgpt|other".
Fixes the rotovap/mushroom-on-phone gap.
- /wiki/memories/{id} (Phase 7E) — full memory detail: content, status,
confidence, refs, valid_until, domain_tags (clickable to domain
pages), project link, source chunk, graduated-to-entity link, full
audit trail, related-by-tag neighbors.
- /wiki/domains/{tag} (Phase 7F) — cross-project view: all active
memories with the given tag grouped by project, sorted by count.
Case-insensitive, whitespace-tolerant. Also surfaces graduated
entities carrying the tag.
- /wiki/activity — autonomous-activity timeline feed. Summary chips
by action (created/promoted/merged/superseded/decayed/canonicalized)
and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay,
phase10-auto-promote, transient-to-durable, tag-canon, human-triage).
Answers "what has the brain been doing while I was away?"
- Home refresh: persistent topnav (Home · Activity · Capture · Triage
· Dashboard), "What the brain is doing" snippet above project cards
showing recent autonomous-actor counts, link to full activity.
Tests: +10 (capture page, memory detail + 404, domain cross-project +
empty + tag normalization, activity feed + groupings, home topnav,
superseded-source detail after merge). 440 → 450.
Known next: capture-browser extension for Claude.ai web (bigger
project, deferred); voice/mobile relay (adjacent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
|
|
|
|
_TOP_NAV_LINKS = [
|
|
|
|
|
|
("🏠 Home", "/wiki"),
|
|
|
|
|
|
("📡 Activity", "/wiki/activity"),
|
|
|
|
|
|
("🔀 Triage", "/admin/triage"),
|
|
|
|
|
|
("📊 Dashboard", "/admin/dashboard"),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _render_topnav(active_path: str = "") -> str:
|
|
|
|
|
|
items = []
|
|
|
|
|
|
for label, href in _TOP_NAV_LINKS:
|
|
|
|
|
|
cls = "topnav-item active" if href == active_path else "topnav-item"
|
|
|
|
|
|
items.append(f'<a href="{href}" class="{cls}">{label}</a>')
|
|
|
|
|
|
return f'<nav class="topnav">{" ".join(items)}</nav>'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_html(title: str, body_html: str, breadcrumbs: list[tuple[str, str]] | None = None, active_path: str = "") -> str:
|
|
|
|
|
|
topnav = _render_topnav(active_path)
|
|
|
|
|
|
crumbs = ""
|
2026-04-13 16:09:12 -04:00
|
|
|
|
if breadcrumbs:
|
|
|
|
|
|
parts = []
|
|
|
|
|
|
for label, href in breadcrumbs:
|
|
|
|
|
|
if href:
|
|
|
|
|
|
parts.append(f'<a href="{href}">{label}</a>')
|
|
|
|
|
|
else:
|
|
|
|
|
|
parts.append(f"<span>{label}</span>")
|
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav)
Closes three gaps the user surfaced: (1) OpenClaw agents run blind
without AtoCore context, (2) mobile/desktop chats can't be captured
at all, (3) wiki UI hadn't kept up with backend capabilities.
Phase 7I — OpenClaw two-way bridge
- Plugin now calls /context/build on before_agent_start and prepends
the context pack to event.prompt, so whatever LLM runs underneath
(sonnet, opus, codex, local model) answers grounded in AtoCore
knowledge. Captured prompt stays the user's original text; fail-open
with a 5s timeout. Config-gated via injectContext flag.
- Plugin version 0.0.0 → 0.2.0; README rewritten.
UI refresh
- /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile
/ ChatGPT / other. Goes through normal /interactions pipeline with
client="claude-desktop|claude-web|claude-mobile|chatgpt|other".
Fixes the rotovap/mushroom-on-phone gap.
- /wiki/memories/{id} (Phase 7E) — full memory detail: content, status,
confidence, refs, valid_until, domain_tags (clickable to domain
pages), project link, source chunk, graduated-to-entity link, full
audit trail, related-by-tag neighbors.
- /wiki/domains/{tag} (Phase 7F) — cross-project view: all active
memories with the given tag grouped by project, sorted by count.
Case-insensitive, whitespace-tolerant. Also surfaces graduated
entities carrying the tag.
- /wiki/activity — autonomous-activity timeline feed. Summary chips
by action (created/promoted/merged/superseded/decayed/canonicalized)
and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay,
phase10-auto-promote, transient-to-durable, tag-canon, human-triage).
Answers "what has the brain been doing while I was away?"
- Home refresh: persistent topnav (Home · Activity · Capture · Triage
· Dashboard), "What the brain is doing" snippet above project cards
showing recent autonomous-actor counts, link to full activity.
Tests: +10 (capture page, memory detail + 404, domain cross-project +
empty + tag normalization, activity feed + groupings, home topnav,
superseded-source detail after merge). 440 → 450.
Known next: capture-browser extension for Claude.ai web (bigger
project, deferred); voice/mobile relay (adjacent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
|
|
|
|
crumbs = f'<nav class="breadcrumbs">{" / ".join(parts)}</nav>'
|
2026-04-13 16:09:12 -04:00
|
|
|
|
|
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav)
Closes three gaps the user surfaced: (1) OpenClaw agents run blind
without AtoCore context, (2) mobile/desktop chats can't be captured
at all, (3) wiki UI hadn't kept up with backend capabilities.
Phase 7I — OpenClaw two-way bridge
- Plugin now calls /context/build on before_agent_start and prepends
the context pack to event.prompt, so whatever LLM runs underneath
(sonnet, opus, codex, local model) answers grounded in AtoCore
knowledge. Captured prompt stays the user's original text; fail-open
with a 5s timeout. Config-gated via injectContext flag.
- Plugin version 0.0.0 → 0.2.0; README rewritten.
UI refresh
- /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile
/ ChatGPT / other. Goes through normal /interactions pipeline with
client="claude-desktop|claude-web|claude-mobile|chatgpt|other".
Fixes the rotovap/mushroom-on-phone gap.
- /wiki/memories/{id} (Phase 7E) — full memory detail: content, status,
confidence, refs, valid_until, domain_tags (clickable to domain
pages), project link, source chunk, graduated-to-entity link, full
audit trail, related-by-tag neighbors.
- /wiki/domains/{tag} (Phase 7F) — cross-project view: all active
memories with the given tag grouped by project, sorted by count.
Case-insensitive, whitespace-tolerant. Also surfaces graduated
entities carrying the tag.
- /wiki/activity — autonomous-activity timeline feed. Summary chips
by action (created/promoted/merged/superseded/decayed/canonicalized)
and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay,
phase10-auto-promote, transient-to-durable, tag-canon, human-triage).
Answers "what has the brain been doing while I was away?"
- Home refresh: persistent topnav (Home · Activity · Capture · Triage
· Dashboard), "What the brain is doing" snippet above project cards
showing recent autonomous-actor counts, link to full activity.
Tests: +10 (capture page, memory detail + 404, domain cross-project +
empty + tag normalization, activity feed + groupings, home topnav,
superseded-source detail after merge). 440 → 450.
Known next: capture-browser extension for Claude.ai web (bigger
project, deferred); voice/mobile relay (adjacent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
|
|
|
|
nav = topnav + crumbs
|
2026-04-13 16:09:12 -04:00
|
|
|
|
return _TEMPLATE.replace("{{title}}", title).replace("{{nav}}", nav).replace("{{body}}", body_html)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_homepage() -> str:
|
|
|
|
|
|
projects = []
|
|
|
|
|
|
try:
|
|
|
|
|
|
registered = load_project_registry()
|
|
|
|
|
|
for p in registered:
|
|
|
|
|
|
entity_count = len(get_entities(project=p.project_id, limit=200))
|
|
|
|
|
|
memory_count = len(get_memories(project=p.project_id, active_only=True, limit=200))
|
2026-04-13 18:47:44 -04:00
|
|
|
|
state_entries = get_state(p.project_id)
|
|
|
|
|
|
|
|
|
|
|
|
# Pull stage/type/client from state entries
|
|
|
|
|
|
stage = ""
|
|
|
|
|
|
proj_type = ""
|
|
|
|
|
|
client = ""
|
|
|
|
|
|
for e in state_entries:
|
|
|
|
|
|
if e.category == "status":
|
|
|
|
|
|
if e.key == "stage":
|
|
|
|
|
|
stage = e.value
|
|
|
|
|
|
elif e.key == "type":
|
|
|
|
|
|
proj_type = e.value
|
|
|
|
|
|
elif e.key == "client":
|
|
|
|
|
|
client = e.value
|
|
|
|
|
|
|
2026-04-13 16:09:12 -04:00
|
|
|
|
projects.append({
|
|
|
|
|
|
"id": p.project_id,
|
|
|
|
|
|
"description": p.description,
|
|
|
|
|
|
"entities": entity_count,
|
|
|
|
|
|
"memories": memory_count,
|
2026-04-13 18:47:44 -04:00
|
|
|
|
"state": len(state_entries),
|
|
|
|
|
|
"stage": stage,
|
|
|
|
|
|
"type": proj_type,
|
|
|
|
|
|
"client": client,
|
2026-04-13 16:09:12 -04:00
|
|
|
|
})
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2026-04-13 18:47:44 -04:00
|
|
|
|
# Group by high-level bucket
|
|
|
|
|
|
buckets: dict[str, list] = {
|
|
|
|
|
|
"Active Contracts": [],
|
|
|
|
|
|
"Leads & Prospects": [],
|
|
|
|
|
|
"Internal Tools & Infra": [],
|
|
|
|
|
|
"Other": [],
|
|
|
|
|
|
}
|
|
|
|
|
|
for p in projects:
|
|
|
|
|
|
t = p["type"].lower()
|
|
|
|
|
|
s = p["stage"].lower()
|
|
|
|
|
|
if "lead" in t or "lead" in s or "prospect" in s:
|
|
|
|
|
|
buckets["Leads & Prospects"].append(p)
|
|
|
|
|
|
elif "contract" in t or ("active" in s and "contract" in s):
|
|
|
|
|
|
buckets["Active Contracts"].append(p)
|
|
|
|
|
|
elif "infra" in t or "tool" in t or "internal" in t:
|
|
|
|
|
|
buckets["Internal Tools & Infra"].append(p)
|
|
|
|
|
|
else:
|
|
|
|
|
|
buckets["Other"].append(p)
|
|
|
|
|
|
|
2026-04-13 16:09:12 -04:00
|
|
|
|
lines = ['<h1>AtoCore Wiki</h1>']
|
|
|
|
|
|
lines.append('<form class="search-box" action="/wiki/search" method="get">')
|
|
|
|
|
|
lines.append('<input type="text" name="q" placeholder="Search entities, memories, projects..." autofocus>')
|
|
|
|
|
|
lines.append('<button type="submit">Search</button>')
|
|
|
|
|
|
lines.append('</form>')
|
|
|
|
|
|
|
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav)
Closes three gaps the user surfaced: (1) OpenClaw agents run blind
without AtoCore context, (2) mobile/desktop chats can't be captured
at all, (3) wiki UI hadn't kept up with backend capabilities.
Phase 7I — OpenClaw two-way bridge
- Plugin now calls /context/build on before_agent_start and prepends
the context pack to event.prompt, so whatever LLM runs underneath
(sonnet, opus, codex, local model) answers grounded in AtoCore
knowledge. Captured prompt stays the user's original text; fail-open
with a 5s timeout. Config-gated via injectContext flag.
- Plugin version 0.0.0 → 0.2.0; README rewritten.
UI refresh
- /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile
/ ChatGPT / other. Goes through normal /interactions pipeline with
client="claude-desktop|claude-web|claude-mobile|chatgpt|other".
Fixes the rotovap/mushroom-on-phone gap.
- /wiki/memories/{id} (Phase 7E) — full memory detail: content, status,
confidence, refs, valid_until, domain_tags (clickable to domain
pages), project link, source chunk, graduated-to-entity link, full
audit trail, related-by-tag neighbors.
- /wiki/domains/{tag} (Phase 7F) — cross-project view: all active
memories with the given tag grouped by project, sorted by count.
Case-insensitive, whitespace-tolerant. Also surfaces graduated
entities carrying the tag.
- /wiki/activity — autonomous-activity timeline feed. Summary chips
by action (created/promoted/merged/superseded/decayed/canonicalized)
and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay,
phase10-auto-promote, transient-to-durable, tag-canon, human-triage).
Answers "what has the brain been doing while I was away?"
- Home refresh: persistent topnav (Home · Activity · Capture · Triage
· Dashboard), "What the brain is doing" snippet above project cards
showing recent autonomous-actor counts, link to full activity.
Tests: +10 (capture page, memory detail + 404, domain cross-project +
empty + tag normalization, activity feed + groupings, home topnav,
superseded-source detail after merge). 440 → 450.
Known next: capture-browser extension for Claude.ai web (bigger
project, deferred); voice/mobile relay (adjacent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
|
|
|
|
# What's happening — autonomous activity snippet
|
|
|
|
|
|
try:
|
|
|
|
|
|
from atocore.memory.service import get_recent_audit
|
|
|
|
|
|
recent = get_recent_audit(limit=30)
|
|
|
|
|
|
by_action: dict[str, int] = {}
|
|
|
|
|
|
by_actor: dict[str, int] = {}
|
|
|
|
|
|
for a in recent:
|
|
|
|
|
|
by_action[a["action"]] = by_action.get(a["action"], 0) + 1
|
|
|
|
|
|
by_actor[a["actor"]] = by_actor.get(a["actor"], 0) + 1
|
|
|
|
|
|
# Surface autonomous actors specifically
|
|
|
|
|
|
auto_actors = {k: v for k, v in by_actor.items()
|
|
|
|
|
|
if k.startswith("auto-") or k == "confidence-decay"
|
|
|
|
|
|
or k == "phase10-auto-promote" or k == "transient-to-durable"}
|
|
|
|
|
|
if recent:
|
|
|
|
|
|
lines.append('<div class="activity-snippet">')
|
|
|
|
|
|
lines.append('<h3>📡 What the brain is doing</h3>')
|
|
|
|
|
|
top_actions = sorted(by_action.items(), key=lambda x: -x[1])[:6]
|
|
|
|
|
|
lines.append('<div class="stat-row">' +
|
|
|
|
|
|
"".join(f'<span>{a}: {n}</span>' for a, n in top_actions) +
|
|
|
|
|
|
'</div>')
|
|
|
|
|
|
if auto_actors:
|
|
|
|
|
|
lines.append(f'<p style="font-size:0.9rem; margin:0.3rem 0;">Autonomous actors: ' +
|
|
|
|
|
|
" · ".join(f'<code>{k}</code> ({v})' for k, v in auto_actors.items()) +
|
|
|
|
|
|
'</p>')
|
|
|
|
|
|
lines.append('<p style="font-size:0.85rem; margin:0;"><a href="/wiki/activity">Full timeline →</a></p>')
|
|
|
|
|
|
lines.append('</div>')
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
feat(entities): inbox + cross-project (project="") support (Issue C)
Makes `inbox` a reserved pseudo-project and `project=""` a first-class
cross-project bucket. Unblocks AKC capturing pre-project leads/quotes
and cross-project facts (materials, vendors) that don't fit a single
registered project.
- projects/registry.py: INBOX_PROJECT/GLOBAL_PROJECT constants,
is_reserved_project(), register/update guards, resolve_project_name
passthrough for "inbox"
- engineering/service.py: get_entities scoping rules (inbox-only,
global-only, real+global default, scope_only=true strict).
promote_entity accepts target_project to retarget on promote
- api/routes.py: GET /entities gains scope_only; POST /entities accepts
project=null as ""; POST /entities/{id}/promote accepts
{target_project, note}
- engineering/wiki.py: homepage shows "Inbox & Global" cards with live
counts linking to scoped lists
- tests/test_inbox_crossproject.py: 15 tests (reserved enforcement,
scoping rules, API shape, promote retargeting)
- DEV-LEDGER.md: session log, test_count 463 -> 478
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 20:17:32 -04:00
|
|
|
|
# Issue C: Inbox + Global pseudo-projects alongside registered projects.
|
|
|
|
|
|
# scope_only=True keeps real-project entities out of these counts.
|
|
|
|
|
|
try:
|
|
|
|
|
|
inbox_count = len(get_entities(
|
|
|
|
|
|
project=INBOX_PROJECT, scope_only=True, limit=500,
|
|
|
|
|
|
))
|
|
|
|
|
|
global_count = len(get_entities(
|
|
|
|
|
|
project=GLOBAL_PROJECT, scope_only=True, limit=500,
|
|
|
|
|
|
))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
inbox_count = 0
|
|
|
|
|
|
global_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
lines.append('<h2>📥 Inbox & Global</h2>')
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
'<p class="emerging-intro">Entities that don\'t belong to a specific '
|
|
|
|
|
|
'project yet. <strong>Inbox</strong> holds pre-project leads and quotes. '
|
|
|
|
|
|
'<strong>Global</strong> holds cross-project facts (material properties, '
|
|
|
|
|
|
'vendor capabilities) that apply everywhere.</p>'
|
|
|
|
|
|
)
|
|
|
|
|
|
lines.append('<div class="card-grid">')
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
f'<a href="/entities?project=inbox&scope_only=true" class="card">'
|
|
|
|
|
|
f'<h3>📥 Inbox</h3>'
|
|
|
|
|
|
f'<p>Pre-project leads, quotes, early conversations.</p>'
|
|
|
|
|
|
f'<div class="stats">{inbox_count} entities</div>'
|
|
|
|
|
|
f'</a>'
|
|
|
|
|
|
)
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
f'<a href="/entities?project=&scope_only=true" class="card">'
|
|
|
|
|
|
f'<h3>🌐 Global</h3>'
|
|
|
|
|
|
f'<p>Cross-project facts: materials, vendors, shared knowledge.</p>'
|
|
|
|
|
|
f'<div class="stats">{global_count} entities</div>'
|
|
|
|
|
|
f'</a>'
|
|
|
|
|
|
)
|
|
|
|
|
|
lines.append('</div>')
|
|
|
|
|
|
|
2026-04-13 18:47:44 -04:00
|
|
|
|
for bucket_name, items in buckets.items():
|
|
|
|
|
|
if not items:
|
|
|
|
|
|
continue
|
|
|
|
|
|
lines.append(f'<h2>{bucket_name}</h2>')
|
|
|
|
|
|
lines.append('<div class="card-grid">')
|
|
|
|
|
|
for p in items:
|
|
|
|
|
|
client_line = f'<div class="client">{p["client"]}</div>' if p["client"] else ''
|
|
|
|
|
|
stage_tag = f'<span class="tag">{p["stage"].split(" — ")[0]}</span>' if p["stage"] else ''
|
|
|
|
|
|
lines.append(f'<a href="/wiki/projects/{p["id"]}" class="card">')
|
|
|
|
|
|
lines.append(f'<h3>{p["id"]} {stage_tag}</h3>')
|
|
|
|
|
|
lines.append(client_line)
|
|
|
|
|
|
lines.append(f'<p>{p["description"][:140]}</p>')
|
|
|
|
|
|
lines.append(f'<div class="stats">{p["entities"]} entities · {p["memories"]} memories · {p["state"]} state</div>')
|
|
|
|
|
|
lines.append('</a>')
|
|
|
|
|
|
lines.append('</div>')
|
2026-04-13 16:09:12 -04:00
|
|
|
|
|
feat: Phase 6 — Living Taxonomy + Universal Capture
Closes two real-use gaps:
1. "APM tool" gap: work done outside Claude Code (desktop, web, phone,
other machine) was invisible to AtoCore.
2. Project discovery gap: manual JSON-file edits required to promote
an emerging theme to a first-class project.
B — atocore_remember MCP tool (scripts/atocore_mcp.py):
- New MCP tool for universal capture from any MCP-aware client
(Claude Desktop, Code, Cursor, Zed, Windsurf, etc.)
- Accepts content (required) + memory_type/project/confidence/
valid_until/domain_tags (all optional with sensible defaults)
- Creates a candidate memory, goes through the existing 3-tier triage
(no bypass — the quality gate catches noise)
- Detailed tool description guides Claude on when to invoke: "remember
this", "save that for later", "don't lose this fact"
- Total tools exposed by MCP server: 14 → 15
C.1 Emerging-concepts detector (scripts/detect_emerging.py):
- Nightly scan of active + candidate memories for:
* Unregistered project names with ≥3 memory occurrences
* Top 20 domain_tags by frequency (emerging categories)
* Active memories with reference_count ≥ 5 + valid_until set
(reinforced transients — candidates for extension)
- Writes findings to atocore/proposals/* project state entries
- Emits "warning" alert via Phase 4 framework the FIRST time a new
project crosses the 5-memory alert threshold (avoids spam)
- Configurable via env vars: ATOCORE_EMERGING_PROJECT_MIN (default 3),
ATOCORE_EMERGING_ALERT_THRESHOLD (default 5), TOP_TAGS_LIMIT (20)
C.2 Registration surface (src/atocore/api/routes.py + wiki.py):
- POST /admin/projects/register-emerging — one-click register with
sensible defaults (ingest_roots auto-filled with
vault:incoming/projects/<id>/ convention). Clears the proposal
from the dashboard list on success.
- Dashboard /admin/dashboard: new "proposals" section with
unregistered_projects + emerging_categories + reinforced_transients.
- Wiki homepage: "📋 Emerging" section rendering each unregistered
project as a card with count + 2 sample memory previews + inline
"📌 Register as project" button that calls the endpoint via fetch,
reloads the page on success.
C.3 Transient-to-durable extension
(src/atocore/memory/service.py + API + cron):
- New extend_reinforced_valid_until() function — scans active memories
with valid_until in the next 30 days and reference_count ≥ 5.
Extends expiry by 90 days. If reference_count ≥ 10, clears expiry
entirely (makes permanent). Writes audit rows via the Phase 4
memory_audit framework with actor="transient-to-durable".
- POST /admin/memory/extend-reinforced — API wrapper for cron.
- Matches the user's intuition: "something transient becomes important
if you keep coming back to it".
Nightly cron (deploy/dalidou/batch-extract.sh):
- Step F2: detect_emerging.py (after F pipeline summary)
- Step F3: /admin/memory/extend-reinforced (before integrity check)
- Both fail-open; errors don't break the pipeline.
Tests: 366 → 374 (+8 for Phase 6):
- 6 tests for extend_reinforced_valid_until covering:
extension path, permanent path, skip far-future, skip low-refs,
skip permanent memories, audit row write
- 2 smoke tests for the detector (imports cleanly, handles empty DB)
- MCP tool changes don't need new tests — the wrapper is pure passthrough
Design decisions documented in plan file:
- atocore_remember deliberately doesn't bypass triage (quality gate)
- Detector is passive (surfaces proposals) not active (auto-registers)
- Sensible ingest-root defaults ("vault:incoming/projects/<id>/")
so registration is one-click with no file-path thinking
- Extension adds 90 days rather than clearing expiry (gradual
permanence earned through sustained reinforcement)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 08:08:55 -04:00
|
|
|
|
# Phase 6 C.2: Emerging projects section
|
|
|
|
|
|
try:
|
|
|
|
|
|
import json as _json
|
|
|
|
|
|
emerging_projects = []
|
|
|
|
|
|
state_entries = get_state("atocore")
|
|
|
|
|
|
for e in state_entries:
|
|
|
|
|
|
if e.category == "proposals" and e.key == "unregistered_projects":
|
|
|
|
|
|
try:
|
|
|
|
|
|
emerging_projects = _json.loads(e.value)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
emerging_projects = []
|
|
|
|
|
|
break
|
|
|
|
|
|
if emerging_projects:
|
|
|
|
|
|
lines.append('<h2>📋 Emerging</h2>')
|
|
|
|
|
|
lines.append('<p class="emerging-intro">Projects that appear in memories but aren\'t yet registered. '
|
|
|
|
|
|
'One click to promote them to first-class projects.</p>')
|
|
|
|
|
|
lines.append('<div class="emerging-grid">')
|
|
|
|
|
|
for ep in emerging_projects[:10]:
|
|
|
|
|
|
name = ep.get("project", "?")
|
|
|
|
|
|
count = ep.get("count", 0)
|
|
|
|
|
|
samples = ep.get("sample_contents", [])
|
|
|
|
|
|
samples_html = "".join(f'<li>{s[:120]}</li>' for s in samples[:2])
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
f'<div class="emerging-card">'
|
|
|
|
|
|
f'<h3>{name}</h3>'
|
|
|
|
|
|
f'<div class="emerging-count">{count} memories</div>'
|
|
|
|
|
|
f'<ul class="emerging-samples">{samples_html}</ul>'
|
|
|
|
|
|
f'<button class="btn-register-emerging" onclick="registerEmerging({name!r})">📌 Register as project</button>'
|
|
|
|
|
|
f'</div>'
|
|
|
|
|
|
)
|
|
|
|
|
|
lines.append('</div>')
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2026-04-13 16:09:12 -04:00
|
|
|
|
# Quick stats
|
|
|
|
|
|
all_entities = get_entities(limit=500)
|
|
|
|
|
|
all_memories = get_memories(active_only=True, limit=500)
|
2026-04-16 20:28:56 -04:00
|
|
|
|
pending = get_memories(status="candidate", limit=500)
|
2026-04-13 16:09:12 -04:00
|
|
|
|
lines.append('<h2>System</h2>')
|
|
|
|
|
|
lines.append(f'<p>{len(all_entities)} entities · {len(all_memories)} active memories · {len(projects)} projects</p>')
|
2026-04-16 20:28:56 -04:00
|
|
|
|
|
|
|
|
|
|
# Triage queue prompt — surfaced prominently if non-empty
|
|
|
|
|
|
if pending:
|
|
|
|
|
|
tone = "triage-warning" if len(pending) > 50 else "triage-notice"
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
f'<p class="{tone}">🗂️ <strong>{len(pending)} candidates</strong> awaiting triage — '
|
|
|
|
|
|
f'<a href="/admin/triage">review now →</a></p>'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
lines.append(f'<p><a href="/admin/triage">Triage Queue</a> · <a href="/admin/dashboard">API Dashboard (JSON)</a> · <a href="/health">Health Check</a></p>')
|
2026-04-13 16:09:12 -04:00
|
|
|
|
|
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav)
Closes three gaps the user surfaced: (1) OpenClaw agents run blind
without AtoCore context, (2) mobile/desktop chats can't be captured
at all, (3) wiki UI hadn't kept up with backend capabilities.
Phase 7I — OpenClaw two-way bridge
- Plugin now calls /context/build on before_agent_start and prepends
the context pack to event.prompt, so whatever LLM runs underneath
(sonnet, opus, codex, local model) answers grounded in AtoCore
knowledge. Captured prompt stays the user's original text; fail-open
with a 5s timeout. Config-gated via injectContext flag.
- Plugin version 0.0.0 → 0.2.0; README rewritten.
UI refresh
- /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile
/ ChatGPT / other. Goes through normal /interactions pipeline with
client="claude-desktop|claude-web|claude-mobile|chatgpt|other".
Fixes the rotovap/mushroom-on-phone gap.
- /wiki/memories/{id} (Phase 7E) — full memory detail: content, status,
confidence, refs, valid_until, domain_tags (clickable to domain
pages), project link, source chunk, graduated-to-entity link, full
audit trail, related-by-tag neighbors.
- /wiki/domains/{tag} (Phase 7F) — cross-project view: all active
memories with the given tag grouped by project, sorted by count.
Case-insensitive, whitespace-tolerant. Also surfaces graduated
entities carrying the tag.
- /wiki/activity — autonomous-activity timeline feed. Summary chips
by action (created/promoted/merged/superseded/decayed/canonicalized)
and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay,
phase10-auto-promote, transient-to-durable, tag-canon, human-triage).
Answers "what has the brain been doing while I was away?"
- Home refresh: persistent topnav (Home · Activity · Capture · Triage
· Dashboard), "What the brain is doing" snippet above project cards
showing recent autonomous-actor counts, link to full activity.
Tests: +10 (capture page, memory detail + 404, domain cross-project +
empty + tag normalization, activity feed + groupings, home topnav,
superseded-source detail after merge). 440 → 450.
Known next: capture-browser extension for Claude.ai web (bigger
project, deferred); voice/mobile relay (adjacent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
|
|
|
|
return render_html("AtoCore Wiki", "\n".join(lines), active_path="/wiki")
|
2026-04-13 16:09:12 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_project(project: str) -> str:
|
|
|
|
|
|
from atocore.engineering.mirror import generate_project_overview
|
|
|
|
|
|
|
|
|
|
|
|
markdown_content = generate_project_overview(project)
|
|
|
|
|
|
# Convert entity names to links
|
|
|
|
|
|
entities = get_entities(project=project, limit=200)
|
|
|
|
|
|
html_body = md.markdown(markdown_content, extensions=["tables", "fenced_code"])
|
|
|
|
|
|
|
|
|
|
|
|
for ent in sorted(entities, key=lambda e: len(e.name), reverse=True):
|
|
|
|
|
|
linked = f'<a href="/wiki/entities/{ent.id}" title="{ent.entity_type}">{ent.name}</a>'
|
|
|
|
|
|
html_body = html_body.replace(f"<strong>{ent.name}</strong>", f"<strong>{linked}</strong>", 1)
|
|
|
|
|
|
|
|
|
|
|
|
return render_html(
|
|
|
|
|
|
f"{project}",
|
|
|
|
|
|
html_body,
|
|
|
|
|
|
breadcrumbs=[("Wiki", "/wiki"), (project, "")],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
feat(assets): binary asset store + artifact entity + wiki evidence (Issue F)
Wires visual evidence into the knowledge graph. Images, PDFs, and CAD
exports can now be uploaded, deduped by SHA-256, thumbnailed, linked to
entities via EVIDENCED_BY, and rendered inline on wiki pages. Unblocks
AKC uploading voice-session screenshots alongside extracted entities.
- assets/ module: store_asset (hash dedup + MIME allowlist + 20 MB cap),
get_asset_binary, get_thumbnail (Pillow, on-disk cache under
.thumbnails/<size>/), list_orphan_assets, invalidate_asset
- models/database.py: new `assets` table + indexes
- engineering/service.py: `artifact` added to ENTITY_TYPES
- api/routes.py: POST /assets (multipart), GET /assets/{id},
/assets/{id}/thumbnail, /assets/{id}/meta, /admin/assets/orphans,
DELETE /assets/{id} (409 if still referenced),
GET /entities/{id}/evidence (EVIDENCED_BY artifacts with asset meta)
- main.py: all new paths aliased under /v1
- engineering/wiki.py: entity pages render EVIDENCED_BY → artifact as a
"Visual evidence" thumbnail strip; artifact pages render the full
image + caption + capture_context
- deploy/dalidou/docker-compose.yml: bind-mount ${ATOCORE_ASSETS_DIR}
- config.py: assets_dir + assets_max_upload_bytes settings
- requirements.txt + pyproject.toml: python-multipart, Pillow>=10.0.0
- tests/test_assets.py: 16 tests (dedup, cap, thumbnail cache, orphan
detection, invalidate gating, API upload/fetch, evidence, v1 aliases,
wiki rendering)
- DEV-LEDGER.md: session log + cleanup note + test_count 478 -> 494
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:46:52 -04:00
|
|
|
|
def _render_visual_evidence(entity_id: str, ctx: dict) -> str:
|
|
|
|
|
|
"""Render EVIDENCED_BY → artifact links as an inline thumbnail strip."""
|
|
|
|
|
|
from atocore.assets import get_asset
|
|
|
|
|
|
|
|
|
|
|
|
artifacts = []
|
|
|
|
|
|
for rel in ctx["relationships"]:
|
|
|
|
|
|
if rel.source_entity_id != entity_id or rel.relationship_type != "evidenced_by":
|
|
|
|
|
|
continue
|
|
|
|
|
|
target = ctx["related_entities"].get(rel.target_entity_id)
|
|
|
|
|
|
if target is None or target.entity_type != "artifact":
|
|
|
|
|
|
continue
|
|
|
|
|
|
artifacts.append(target)
|
|
|
|
|
|
|
|
|
|
|
|
if not artifacts:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
tiles = []
|
|
|
|
|
|
for art in artifacts:
|
|
|
|
|
|
props = art.properties or {}
|
|
|
|
|
|
kind = props.get("kind", "other")
|
|
|
|
|
|
caption = props.get("caption", art.name)
|
|
|
|
|
|
asset_id = props.get("asset_id")
|
|
|
|
|
|
asset = get_asset(asset_id) if asset_id else None
|
|
|
|
|
|
detail_href = f"/wiki/entities/{art.id}"
|
|
|
|
|
|
|
|
|
|
|
|
if kind == "image" and asset and asset.mime_type.startswith("image/"):
|
|
|
|
|
|
full_href = f"/assets/{asset.id}"
|
|
|
|
|
|
thumb = f"/assets/{asset.id}/thumbnail?size=240"
|
|
|
|
|
|
tiles.append(
|
|
|
|
|
|
f'<figure class="evidence-tile">'
|
|
|
|
|
|
f'<a href="{full_href}" target="_blank" rel="noopener">'
|
|
|
|
|
|
f'<img src="{thumb}" alt="{_escape_attr(caption)}" loading="lazy">'
|
|
|
|
|
|
f'</a>'
|
|
|
|
|
|
f'<figcaption><a href="{detail_href}">{_escape_html(caption)}</a></figcaption>'
|
|
|
|
|
|
f'</figure>'
|
|
|
|
|
|
)
|
|
|
|
|
|
elif kind == "pdf" and asset:
|
|
|
|
|
|
full_href = f"/assets/{asset.id}"
|
|
|
|
|
|
tiles.append(
|
|
|
|
|
|
f'<div class="evidence-tile evidence-pdf">'
|
|
|
|
|
|
f'<a href="{full_href}" target="_blank" rel="noopener">'
|
|
|
|
|
|
f'📄 PDF: {_escape_html(caption)}</a>'
|
|
|
|
|
|
f' · <a href="{detail_href}">details</a>'
|
|
|
|
|
|
f'</div>'
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
tiles.append(
|
|
|
|
|
|
f'<div class="evidence-tile evidence-other">'
|
|
|
|
|
|
f'<a href="{detail_href}">📎 {_escape_html(caption)}</a>'
|
|
|
|
|
|
f' <span class="tag">{kind}</span>'
|
|
|
|
|
|
f'</div>'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
'<h2>Visual evidence</h2>'
|
|
|
|
|
|
f'<div class="evidence-strip">{"".join(tiles)}</div>'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _render_artifact_body(ent) -> list[str]:
|
|
|
|
|
|
"""Render an artifact entity's own image/pdf/caption."""
|
|
|
|
|
|
from atocore.assets import get_asset
|
|
|
|
|
|
|
|
|
|
|
|
props = ent.properties or {}
|
|
|
|
|
|
kind = props.get("kind", "other")
|
|
|
|
|
|
caption = props.get("caption", "")
|
|
|
|
|
|
capture_context = props.get("capture_context", "")
|
|
|
|
|
|
asset_id = props.get("asset_id")
|
|
|
|
|
|
asset = get_asset(asset_id) if asset_id else None
|
|
|
|
|
|
|
|
|
|
|
|
out: list[str] = []
|
|
|
|
|
|
if kind == "image" and asset and asset.mime_type.startswith("image/"):
|
|
|
|
|
|
out.append(
|
|
|
|
|
|
f'<figure class="artifact-full">'
|
|
|
|
|
|
f'<a href="/assets/{asset.id}" target="_blank" rel="noopener">'
|
|
|
|
|
|
f'<img src="/assets/{asset.id}/thumbnail?size=1024" '
|
|
|
|
|
|
f'alt="{_escape_attr(caption or ent.name)}">'
|
|
|
|
|
|
f'</a>'
|
|
|
|
|
|
f'<figcaption>{_escape_html(caption)}</figcaption>'
|
|
|
|
|
|
f'</figure>'
|
|
|
|
|
|
)
|
|
|
|
|
|
elif kind == "pdf" and asset:
|
|
|
|
|
|
out.append(
|
|
|
|
|
|
f'<p>📄 <a href="/assets/{asset.id}" target="_blank" rel="noopener">'
|
|
|
|
|
|
f'Open PDF ({asset.size_bytes // 1024} KB)</a></p>'
|
|
|
|
|
|
)
|
|
|
|
|
|
elif asset_id:
|
|
|
|
|
|
out.append(f'<p class="meta">asset_id: <code>{asset_id}</code> — blob missing</p>')
|
|
|
|
|
|
|
|
|
|
|
|
if capture_context:
|
|
|
|
|
|
out.append('<h2>Capture context</h2>')
|
|
|
|
|
|
out.append(f'<blockquote>{_escape_html(capture_context)}</blockquote>')
|
|
|
|
|
|
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _escape_html(s: str) -> str:
|
|
|
|
|
|
if s is None:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
return (str(s)
|
|
|
|
|
|
.replace("&", "&")
|
|
|
|
|
|
.replace("<", "<")
|
|
|
|
|
|
.replace(">", ">"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _escape_attr(s: str) -> str:
|
|
|
|
|
|
return _escape_html(s).replace('"', """)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-13 16:09:12 -04:00
|
|
|
|
def render_entity(entity_id: str) -> str | None:
|
|
|
|
|
|
ctx = get_entity_with_context(entity_id)
|
|
|
|
|
|
if ctx is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
ent = ctx["entity"]
|
|
|
|
|
|
lines = [f'<h1>[{ent.entity_type}] {ent.name}</h1>']
|
|
|
|
|
|
|
|
|
|
|
|
if ent.project:
|
|
|
|
|
|
lines.append(f'<p>Project: <a href="/wiki/projects/{ent.project}">{ent.project}</a></p>')
|
|
|
|
|
|
if ent.description:
|
|
|
|
|
|
lines.append(f'<p>{ent.description}</p>')
|
|
|
|
|
|
if ent.properties:
|
|
|
|
|
|
lines.append('<h2>Properties</h2><ul>')
|
|
|
|
|
|
for k, v in ent.properties.items():
|
|
|
|
|
|
lines.append(f'<li><strong>{k}</strong>: {v}</li>')
|
|
|
|
|
|
lines.append('</ul>')
|
|
|
|
|
|
|
|
|
|
|
|
lines.append(f'<p class="meta">confidence: {ent.confidence} · status: {ent.status} · created: {ent.created_at}</p>')
|
|
|
|
|
|
|
feat(assets): binary asset store + artifact entity + wiki evidence (Issue F)
Wires visual evidence into the knowledge graph. Images, PDFs, and CAD
exports can now be uploaded, deduped by SHA-256, thumbnailed, linked to
entities via EVIDENCED_BY, and rendered inline on wiki pages. Unblocks
AKC uploading voice-session screenshots alongside extracted entities.
- assets/ module: store_asset (hash dedup + MIME allowlist + 20 MB cap),
get_asset_binary, get_thumbnail (Pillow, on-disk cache under
.thumbnails/<size>/), list_orphan_assets, invalidate_asset
- models/database.py: new `assets` table + indexes
- engineering/service.py: `artifact` added to ENTITY_TYPES
- api/routes.py: POST /assets (multipart), GET /assets/{id},
/assets/{id}/thumbnail, /assets/{id}/meta, /admin/assets/orphans,
DELETE /assets/{id} (409 if still referenced),
GET /entities/{id}/evidence (EVIDENCED_BY artifacts with asset meta)
- main.py: all new paths aliased under /v1
- engineering/wiki.py: entity pages render EVIDENCED_BY → artifact as a
"Visual evidence" thumbnail strip; artifact pages render the full
image + caption + capture_context
- deploy/dalidou/docker-compose.yml: bind-mount ${ATOCORE_ASSETS_DIR}
- config.py: assets_dir + assets_max_upload_bytes settings
- requirements.txt + pyproject.toml: python-multipart, Pillow>=10.0.0
- tests/test_assets.py: 16 tests (dedup, cap, thumbnail cache, orphan
detection, invalidate gating, API upload/fetch, evidence, v1 aliases,
wiki rendering)
- DEV-LEDGER.md: session log + cleanup note + test_count 478 -> 494
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:46:52 -04:00
|
|
|
|
# Issue F: artifact entities render their own image inline; other
|
|
|
|
|
|
# entities render their EVIDENCED_BY artifacts as a visual strip.
|
|
|
|
|
|
if ent.entity_type == "artifact":
|
|
|
|
|
|
lines.extend(_render_artifact_body(ent))
|
|
|
|
|
|
else:
|
|
|
|
|
|
evidence_html = _render_visual_evidence(ent.id, ctx)
|
|
|
|
|
|
if evidence_html:
|
|
|
|
|
|
lines.append(evidence_html)
|
|
|
|
|
|
|
2026-04-13 16:09:12 -04:00
|
|
|
|
if ctx["relationships"]:
|
|
|
|
|
|
lines.append('<h2>Relationships</h2><ul>')
|
|
|
|
|
|
for rel in ctx["relationships"]:
|
|
|
|
|
|
other_id = rel.target_entity_id if rel.source_entity_id == entity_id else rel.source_entity_id
|
|
|
|
|
|
other = ctx["related_entities"].get(other_id)
|
|
|
|
|
|
if other:
|
|
|
|
|
|
direction = "\u2192" if rel.source_entity_id == entity_id else "\u2190"
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
f'<li>{direction} <em>{rel.relationship_type}</em> '
|
|
|
|
|
|
f'<a href="/wiki/entities/{other_id}">[{other.entity_type}] {other.name}</a></li>'
|
|
|
|
|
|
)
|
|
|
|
|
|
lines.append('</ul>')
|
|
|
|
|
|
|
|
|
|
|
|
breadcrumbs = [("Wiki", "/wiki")]
|
|
|
|
|
|
if ent.project:
|
|
|
|
|
|
breadcrumbs.append((ent.project, f"/wiki/projects/{ent.project}"))
|
|
|
|
|
|
breadcrumbs.append((ent.name, ""))
|
|
|
|
|
|
|
|
|
|
|
|
return render_html(ent.name, "\n".join(lines), breadcrumbs=breadcrumbs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_search(query: str) -> str:
|
|
|
|
|
|
lines = [f'<h1>Search: "{query}"</h1>']
|
|
|
|
|
|
|
|
|
|
|
|
# Search entities by name
|
|
|
|
|
|
entities = get_entities(name_contains=query, limit=20)
|
|
|
|
|
|
if entities:
|
|
|
|
|
|
lines.append(f'<h2>Entities ({len(entities)})</h2><ul>')
|
|
|
|
|
|
for e in entities:
|
|
|
|
|
|
proj = f' <span class="tag">{e.project}</span>' if e.project else ''
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
f'<li><a href="/wiki/entities/{e.id}">[{e.entity_type}] {e.name}</a>{proj}'
|
|
|
|
|
|
f'{" — " + e.description[:100] if e.description else ""}</li>'
|
|
|
|
|
|
)
|
|
|
|
|
|
lines.append('</ul>')
|
|
|
|
|
|
|
feat: Phase 3 V1 — Auto-Organization (domain_tags + valid_until)
Adds structural metadata that the LLM triage was already implicitly
reasoning about ("stale snapshot" → reject). Phase 3 captures that
reasoning as fields so it can DRIVE retrieval, not just rejection.
Schema (src/atocore/models/database.py):
- domain_tags TEXT DEFAULT '[]' JSON array of lowercase topic keywords
- valid_until DATETIME ISO date; null = permanent
- idx_memories_valid_until index for efficient expiry queries
Memory service (src/atocore/memory/service.py):
- Memory dataclass gains domain_tags + valid_until
- create_memory, update_memory accept/persist both
- _row_to_memory safely reads both (JSON-decode + null handling)
- _normalize_tags helper: lowercase, dedup, strip, cap at 10
- get_memories_for_context filters expired (valid_until < today UTC)
- _rank_memories_for_query adds tag-boost: memories whose domain_tags
appear as substrings in query text rank higher (tertiary key after
content-overlap density + absolute overlap, before confidence)
LLM extractor (_llm_prompt.py → llm-0.5.0):
- SYSTEM_PROMPT documents domain_tags (2-5 keywords) + valid_until
(time-bounded facts get expiry dates; durable facts stay null)
- normalize_candidate_item parses both fields from model output with
graceful fallback for string/null/missing
LLM triage (scripts/auto_triage.py):
- TRIAGE_SYSTEM_PROMPT documents same two fields
- parse_verdict extracts them from verdict JSON
- On promote: PUT /memory/{id} with tags + valid_until BEFORE
POST /memory/{id}/promote, so active memories carry them
API (src/atocore/api/routes.py):
- MemoryCreateRequest: adds domain_tags, valid_until
- MemoryUpdateRequest: adds domain_tags, valid_until, memory_type
- GET /memory response exposes domain_tags + valid_until + created_at
Triage UI (src/atocore/engineering/triage_ui.py):
- Renders existing tags as colored badges
- Adds inline text field for tags (comma-separated) + date picker for
valid_until on every candidate card
- Save&Promote button persists edits via PUT then promotes
- Plain Promote (and Y shortcut) also saves tags/expiry if edited
Wiki (src/atocore/engineering/wiki.py):
- Search now matches memory content OR domain_tags
- Search results render tags as clickable badges linking to
/wiki/search?q=<tag> for cross-project navigation
- valid_until shown as amber "valid until YYYY-MM-DD" hint
Tests: 303 → 308 (5 new for Phase 3 behavior):
- test_create_memory_with_tags_and_valid_until
- test_create_memory_normalizes_tags
- test_update_memory_sets_tags_and_valid_until
- test_get_memories_for_context_excludes_expired
- test_context_builder_tag_boost_orders_results
Deferred (explicitly): temporal_scope enum, source_refs memory graph,
HDBSCAN clustering, memory detail wiki page, backfill of existing
actives. See docs/MASTER-BRAIN-PLAN.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:37:01 -04:00
|
|
|
|
# Search memories — match on content OR domain_tags (Phase 3)
|
2026-04-13 16:09:12 -04:00
|
|
|
|
all_memories = get_memories(active_only=True, limit=200)
|
|
|
|
|
|
query_lower = query.lower()
|
feat: Phase 3 V1 — Auto-Organization (domain_tags + valid_until)
Adds structural metadata that the LLM triage was already implicitly
reasoning about ("stale snapshot" → reject). Phase 3 captures that
reasoning as fields so it can DRIVE retrieval, not just rejection.
Schema (src/atocore/models/database.py):
- domain_tags TEXT DEFAULT '[]' JSON array of lowercase topic keywords
- valid_until DATETIME ISO date; null = permanent
- idx_memories_valid_until index for efficient expiry queries
Memory service (src/atocore/memory/service.py):
- Memory dataclass gains domain_tags + valid_until
- create_memory, update_memory accept/persist both
- _row_to_memory safely reads both (JSON-decode + null handling)
- _normalize_tags helper: lowercase, dedup, strip, cap at 10
- get_memories_for_context filters expired (valid_until < today UTC)
- _rank_memories_for_query adds tag-boost: memories whose domain_tags
appear as substrings in query text rank higher (tertiary key after
content-overlap density + absolute overlap, before confidence)
LLM extractor (_llm_prompt.py → llm-0.5.0):
- SYSTEM_PROMPT documents domain_tags (2-5 keywords) + valid_until
(time-bounded facts get expiry dates; durable facts stay null)
- normalize_candidate_item parses both fields from model output with
graceful fallback for string/null/missing
LLM triage (scripts/auto_triage.py):
- TRIAGE_SYSTEM_PROMPT documents same two fields
- parse_verdict extracts them from verdict JSON
- On promote: PUT /memory/{id} with tags + valid_until BEFORE
POST /memory/{id}/promote, so active memories carry them
API (src/atocore/api/routes.py):
- MemoryCreateRequest: adds domain_tags, valid_until
- MemoryUpdateRequest: adds domain_tags, valid_until, memory_type
- GET /memory response exposes domain_tags + valid_until + created_at
Triage UI (src/atocore/engineering/triage_ui.py):
- Renders existing tags as colored badges
- Adds inline text field for tags (comma-separated) + date picker for
valid_until on every candidate card
- Save&Promote button persists edits via PUT then promotes
- Plain Promote (and Y shortcut) also saves tags/expiry if edited
Wiki (src/atocore/engineering/wiki.py):
- Search now matches memory content OR domain_tags
- Search results render tags as clickable badges linking to
/wiki/search?q=<tag> for cross-project navigation
- valid_until shown as amber "valid until YYYY-MM-DD" hint
Tests: 303 → 308 (5 new for Phase 3 behavior):
- test_create_memory_with_tags_and_valid_until
- test_create_memory_normalizes_tags
- test_update_memory_sets_tags_and_valid_until
- test_get_memories_for_context_excludes_expired
- test_context_builder_tag_boost_orders_results
Deferred (explicitly): temporal_scope enum, source_refs memory graph,
HDBSCAN clustering, memory detail wiki page, backfill of existing
actives. See docs/MASTER-BRAIN-PLAN.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:37:01 -04:00
|
|
|
|
matching_mems = [
|
|
|
|
|
|
m for m in all_memories
|
|
|
|
|
|
if query_lower in m.content.lower()
|
|
|
|
|
|
or any(query_lower in (t or "").lower() for t in (m.domain_tags or []))
|
|
|
|
|
|
][:20]
|
2026-04-13 16:09:12 -04:00
|
|
|
|
if matching_mems:
|
|
|
|
|
|
lines.append(f'<h2>Memories ({len(matching_mems)})</h2><ul>')
|
|
|
|
|
|
for m in matching_mems:
|
|
|
|
|
|
proj = f' <span class="tag">{m.project}</span>' if m.project else ''
|
feat: Phase 3 V1 — Auto-Organization (domain_tags + valid_until)
Adds structural metadata that the LLM triage was already implicitly
reasoning about ("stale snapshot" → reject). Phase 3 captures that
reasoning as fields so it can DRIVE retrieval, not just rejection.
Schema (src/atocore/models/database.py):
- domain_tags TEXT DEFAULT '[]' JSON array of lowercase topic keywords
- valid_until DATETIME ISO date; null = permanent
- idx_memories_valid_until index for efficient expiry queries
Memory service (src/atocore/memory/service.py):
- Memory dataclass gains domain_tags + valid_until
- create_memory, update_memory accept/persist both
- _row_to_memory safely reads both (JSON-decode + null handling)
- _normalize_tags helper: lowercase, dedup, strip, cap at 10
- get_memories_for_context filters expired (valid_until < today UTC)
- _rank_memories_for_query adds tag-boost: memories whose domain_tags
appear as substrings in query text rank higher (tertiary key after
content-overlap density + absolute overlap, before confidence)
LLM extractor (_llm_prompt.py → llm-0.5.0):
- SYSTEM_PROMPT documents domain_tags (2-5 keywords) + valid_until
(time-bounded facts get expiry dates; durable facts stay null)
- normalize_candidate_item parses both fields from model output with
graceful fallback for string/null/missing
LLM triage (scripts/auto_triage.py):
- TRIAGE_SYSTEM_PROMPT documents same two fields
- parse_verdict extracts them from verdict JSON
- On promote: PUT /memory/{id} with tags + valid_until BEFORE
POST /memory/{id}/promote, so active memories carry them
API (src/atocore/api/routes.py):
- MemoryCreateRequest: adds domain_tags, valid_until
- MemoryUpdateRequest: adds domain_tags, valid_until, memory_type
- GET /memory response exposes domain_tags + valid_until + created_at
Triage UI (src/atocore/engineering/triage_ui.py):
- Renders existing tags as colored badges
- Adds inline text field for tags (comma-separated) + date picker for
valid_until on every candidate card
- Save&Promote button persists edits via PUT then promotes
- Plain Promote (and Y shortcut) also saves tags/expiry if edited
Wiki (src/atocore/engineering/wiki.py):
- Search now matches memory content OR domain_tags
- Search results render tags as clickable badges linking to
/wiki/search?q=<tag> for cross-project navigation
- valid_until shown as amber "valid until YYYY-MM-DD" hint
Tests: 303 → 308 (5 new for Phase 3 behavior):
- test_create_memory_with_tags_and_valid_until
- test_create_memory_normalizes_tags
- test_update_memory_sets_tags_and_valid_until
- test_get_memories_for_context_excludes_expired
- test_context_builder_tag_boost_orders_results
Deferred (explicitly): temporal_scope enum, source_refs memory graph,
HDBSCAN clustering, memory detail wiki page, backfill of existing
actives. See docs/MASTER-BRAIN-PLAN.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:37:01 -04:00
|
|
|
|
tags_html = ""
|
|
|
|
|
|
if m.domain_tags:
|
|
|
|
|
|
tag_links = " ".join(
|
|
|
|
|
|
f'<a href="/wiki/search?q={t}" class="tag-badge">{t}</a>'
|
|
|
|
|
|
for t in m.domain_tags[:5]
|
|
|
|
|
|
)
|
|
|
|
|
|
tags_html = f' <span class="mem-tags">{tag_links}</span>'
|
|
|
|
|
|
expiry_html = ""
|
|
|
|
|
|
if m.valid_until:
|
|
|
|
|
|
expiry_html = f' <span class="mem-expiry">valid until {m.valid_until[:10]}</span>'
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
f'<li>[{m.memory_type}]{proj}{tags_html}{expiry_html} '
|
|
|
|
|
|
f'{m.content[:200]}</li>'
|
|
|
|
|
|
)
|
2026-04-13 16:09:12 -04:00
|
|
|
|
lines.append('</ul>')
|
|
|
|
|
|
|
|
|
|
|
|
if not entities and not matching_mems:
|
|
|
|
|
|
lines.append('<p>No results found.</p>')
|
|
|
|
|
|
|
|
|
|
|
|
lines.append('<form class="search-box" action="/wiki/search" method="get">')
|
|
|
|
|
|
lines.append(f'<input type="text" name="q" value="{query}" autofocus>')
|
|
|
|
|
|
lines.append('<button type="submit">Search</button>')
|
|
|
|
|
|
lines.append('</form>')
|
|
|
|
|
|
|
|
|
|
|
|
return render_html(
|
|
|
|
|
|
f"Search: {query}",
|
|
|
|
|
|
"\n".join(lines),
|
|
|
|
|
|
breadcrumbs=[("Wiki", "/wiki"), ("Search", "")],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav)
Closes three gaps the user surfaced: (1) OpenClaw agents run blind
without AtoCore context, (2) mobile/desktop chats can't be captured
at all, (3) wiki UI hadn't kept up with backend capabilities.
Phase 7I — OpenClaw two-way bridge
- Plugin now calls /context/build on before_agent_start and prepends
the context pack to event.prompt, so whatever LLM runs underneath
(sonnet, opus, codex, local model) answers grounded in AtoCore
knowledge. Captured prompt stays the user's original text; fail-open
with a 5s timeout. Config-gated via injectContext flag.
- Plugin version 0.0.0 → 0.2.0; README rewritten.
UI refresh
- /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile
/ ChatGPT / other. Goes through normal /interactions pipeline with
client="claude-desktop|claude-web|claude-mobile|chatgpt|other".
Fixes the rotovap/mushroom-on-phone gap.
- /wiki/memories/{id} (Phase 7E) — full memory detail: content, status,
confidence, refs, valid_until, domain_tags (clickable to domain
pages), project link, source chunk, graduated-to-entity link, full
audit trail, related-by-tag neighbors.
- /wiki/domains/{tag} (Phase 7F) — cross-project view: all active
memories with the given tag grouped by project, sorted by count.
Case-insensitive, whitespace-tolerant. Also surfaces graduated
entities carrying the tag.
- /wiki/activity — autonomous-activity timeline feed. Summary chips
by action (created/promoted/merged/superseded/decayed/canonicalized)
and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay,
phase10-auto-promote, transient-to-durable, tag-canon, human-triage).
Answers "what has the brain been doing while I was away?"
- Home refresh: persistent topnav (Home · Activity · Capture · Triage
· Dashboard), "What the brain is doing" snippet above project cards
showing recent autonomous-actor counts, link to full activity.
Tests: +10 (capture page, memory detail + 404, domain cross-project +
empty + tag normalization, activity feed + groupings, home topnav,
superseded-source detail after merge). 440 → 450.
Known next: capture-browser extension for Claude.ai web (bigger
project, deferred); voice/mobile relay (adjacent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
|
|
|
|
# ---------------------------------------------------------------------
|
2026-04-19 12:01:41 -04:00
|
|
|
|
# /wiki/capture — DEPRECATED emergency paste-in form.
|
|
|
|
|
|
# Kept as an endpoint because POST /interactions is public anyway, but
|
|
|
|
|
|
# REMOVED from the topnav so it's not promoted as the capture path.
|
|
|
|
|
|
# The sanctioned surfaces are Claude Code (Stop + UserPromptSubmit
|
|
|
|
|
|
# hooks) and OpenClaw (capture plugin with 7I context injection).
|
|
|
|
|
|
# This form is explicitly a last-resort for when someone has to feed
|
|
|
|
|
|
# in an external log and can't get the normal hooks to reach it.
|
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav)
Closes three gaps the user surfaced: (1) OpenClaw agents run blind
without AtoCore context, (2) mobile/desktop chats can't be captured
at all, (3) wiki UI hadn't kept up with backend capabilities.
Phase 7I — OpenClaw two-way bridge
- Plugin now calls /context/build on before_agent_start and prepends
the context pack to event.prompt, so whatever LLM runs underneath
(sonnet, opus, codex, local model) answers grounded in AtoCore
knowledge. Captured prompt stays the user's original text; fail-open
with a 5s timeout. Config-gated via injectContext flag.
- Plugin version 0.0.0 → 0.2.0; README rewritten.
UI refresh
- /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile
/ ChatGPT / other. Goes through normal /interactions pipeline with
client="claude-desktop|claude-web|claude-mobile|chatgpt|other".
Fixes the rotovap/mushroom-on-phone gap.
- /wiki/memories/{id} (Phase 7E) — full memory detail: content, status,
confidence, refs, valid_until, domain_tags (clickable to domain
pages), project link, source chunk, graduated-to-entity link, full
audit trail, related-by-tag neighbors.
- /wiki/domains/{tag} (Phase 7F) — cross-project view: all active
memories with the given tag grouped by project, sorted by count.
Case-insensitive, whitespace-tolerant. Also surfaces graduated
entities carrying the tag.
- /wiki/activity — autonomous-activity timeline feed. Summary chips
by action (created/promoted/merged/superseded/decayed/canonicalized)
and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay,
phase10-auto-promote, transient-to-durable, tag-canon, human-triage).
Answers "what has the brain been doing while I was away?"
- Home refresh: persistent topnav (Home · Activity · Capture · Triage
· Dashboard), "What the brain is doing" snippet above project cards
showing recent autonomous-actor counts, link to full activity.
Tests: +10 (capture page, memory detail + 404, domain cross-project +
empty + tag normalization, activity feed + groupings, home topnav,
superseded-source detail after merge). 440 → 450.
Known next: capture-browser extension for Claude.ai web (bigger
project, deferred); voice/mobile relay (adjacent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_capture() -> str:
|
2026-04-19 12:01:41 -04:00
|
|
|
|
lines = ['<h1>📥 Manual capture (fallback only)</h1>']
|
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav)
Closes three gaps the user surfaced: (1) OpenClaw agents run blind
without AtoCore context, (2) mobile/desktop chats can't be captured
at all, (3) wiki UI hadn't kept up with backend capabilities.
Phase 7I — OpenClaw two-way bridge
- Plugin now calls /context/build on before_agent_start and prepends
the context pack to event.prompt, so whatever LLM runs underneath
(sonnet, opus, codex, local model) answers grounded in AtoCore
knowledge. Captured prompt stays the user's original text; fail-open
with a 5s timeout. Config-gated via injectContext flag.
- Plugin version 0.0.0 → 0.2.0; README rewritten.
UI refresh
- /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile
/ ChatGPT / other. Goes through normal /interactions pipeline with
client="claude-desktop|claude-web|claude-mobile|chatgpt|other".
Fixes the rotovap/mushroom-on-phone gap.
- /wiki/memories/{id} (Phase 7E) — full memory detail: content, status,
confidence, refs, valid_until, domain_tags (clickable to domain
pages), project link, source chunk, graduated-to-entity link, full
audit trail, related-by-tag neighbors.
- /wiki/domains/{tag} (Phase 7F) — cross-project view: all active
memories with the given tag grouped by project, sorted by count.
Case-insensitive, whitespace-tolerant. Also surfaces graduated
entities carrying the tag.
- /wiki/activity — autonomous-activity timeline feed. Summary chips
by action (created/promoted/merged/superseded/decayed/canonicalized)
and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay,
phase10-auto-promote, transient-to-durable, tag-canon, human-triage).
Answers "what has the brain been doing while I was away?"
- Home refresh: persistent topnav (Home · Activity · Capture · Triage
· Dashboard), "What the brain is doing" snippet above project cards
showing recent autonomous-actor counts, link to full activity.
Tests: +10 (capture page, memory detail + 404, domain cross-project +
empty + tag normalization, activity feed + groupings, home topnav,
superseded-source detail after merge). 440 → 450.
Known next: capture-browser extension for Claude.ai web (bigger
project, deferred); voice/mobile relay (adjacent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
|
|
|
|
lines.append(
|
2026-04-19 12:01:41 -04:00
|
|
|
|
'<div class="triage-warning"><strong>This is not the capture path.</strong> '
|
|
|
|
|
|
'The sanctioned capture surfaces are Claude Code (Stop hook auto-captures every turn) '
|
|
|
|
|
|
'and OpenClaw (plugin auto-captures + injects AtoCore context on every agent turn). '
|
|
|
|
|
|
'This form exists only as a last resort for external logs you can\'t get into the normal pipeline.</div>'
|
|
|
|
|
|
)
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
'<p>If you\'re reaching for this page because you had a chat somewhere AtoCore didn\'t see, '
|
|
|
|
|
|
'fix the capture surface instead — don\'t paste. The deliberate scope is Claude Code + OpenClaw.</p>'
|
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav)
Closes three gaps the user surfaced: (1) OpenClaw agents run blind
without AtoCore context, (2) mobile/desktop chats can't be captured
at all, (3) wiki UI hadn't kept up with backend capabilities.
Phase 7I — OpenClaw two-way bridge
- Plugin now calls /context/build on before_agent_start and prepends
the context pack to event.prompt, so whatever LLM runs underneath
(sonnet, opus, codex, local model) answers grounded in AtoCore
knowledge. Captured prompt stays the user's original text; fail-open
with a 5s timeout. Config-gated via injectContext flag.
- Plugin version 0.0.0 → 0.2.0; README rewritten.
UI refresh
- /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile
/ ChatGPT / other. Goes through normal /interactions pipeline with
client="claude-desktop|claude-web|claude-mobile|chatgpt|other".
Fixes the rotovap/mushroom-on-phone gap.
- /wiki/memories/{id} (Phase 7E) — full memory detail: content, status,
confidence, refs, valid_until, domain_tags (clickable to domain
pages), project link, source chunk, graduated-to-entity link, full
audit trail, related-by-tag neighbors.
- /wiki/domains/{tag} (Phase 7F) — cross-project view: all active
memories with the given tag grouped by project, sorted by count.
Case-insensitive, whitespace-tolerant. Also surfaces graduated
entities carrying the tag.
- /wiki/activity — autonomous-activity timeline feed. Summary chips
by action (created/promoted/merged/superseded/decayed/canonicalized)
and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay,
phase10-auto-promote, transient-to-durable, tag-canon, human-triage).
Answers "what has the brain been doing while I was away?"
- Home refresh: persistent topnav (Home · Activity · Capture · Triage
· Dashboard), "What the brain is doing" snippet above project cards
showing recent autonomous-actor counts, link to full activity.
Tests: +10 (capture page, memory detail + 404, domain cross-project +
empty + tag normalization, activity feed + groupings, home topnav,
superseded-source detail after merge). 440 → 450.
Known next: capture-browser extension for Claude.ai web (bigger
project, deferred); voice/mobile relay (adjacent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
|
|
|
|
)
|
|
|
|
|
|
lines.append('<p class="meta">Your prompt + the assistant\'s response. Project is optional — '
|
|
|
|
|
|
'the extractor infers it from content.</p>')
|
|
|
|
|
|
lines.append("""
|
|
|
|
|
|
<form id="capture-form" style="display:flex; flex-direction:column; gap:0.8rem; margin-top:1rem;">
|
|
|
|
|
|
<label><strong>Your prompt / question</strong>
|
|
|
|
|
|
<textarea id="cap-prompt" required rows="4"
|
|
|
|
|
|
style="width:100%; padding:0.6rem; background:var(--bg); color:var(--text); border:1px solid var(--border); border-radius:6px; font-family:inherit; font-size:0.95rem;"
|
|
|
|
|
|
placeholder="Paste what you asked…"></textarea>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label><strong>Assistant response</strong>
|
|
|
|
|
|
<textarea id="cap-response" required rows="10"
|
|
|
|
|
|
style="width:100%; padding:0.6rem; background:var(--bg); color:var(--text); border:1px solid var(--border); border-radius:6px; font-family:inherit; font-size:0.95rem;"
|
|
|
|
|
|
placeholder="Paste the full assistant response…"></textarea>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<div style="display:flex; gap:0.5rem; align-items:center; flex-wrap:wrap;">
|
|
|
|
|
|
<label style="display:flex; gap:0.35rem; align-items:center;">Project (optional):
|
|
|
|
|
|
<input type="text" id="cap-project" placeholder="auto-detect"
|
|
|
|
|
|
style="padding:0.35rem 0.6rem; background:var(--bg); color:var(--text); border:1px solid var(--border); border-radius:4px; font-family:monospace; width:180px;">
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label style="display:flex; gap:0.35rem; align-items:center;">Source:
|
|
|
|
|
|
<select id="cap-source" style="padding:0.35rem; background:var(--bg); color:var(--text); border:1px solid var(--border); border-radius:4px;">
|
|
|
|
|
|
<option value="claude-desktop">Claude Desktop</option>
|
|
|
|
|
|
<option value="claude-web">Claude.ai web</option>
|
|
|
|
|
|
<option value="claude-mobile">Claude mobile</option>
|
|
|
|
|
|
<option value="chatgpt">ChatGPT</option>
|
|
|
|
|
|
<option value="other">Other</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button type="submit"
|
|
|
|
|
|
style="padding:0.6rem 1.2rem; background:var(--accent); color:white; border:none; border-radius:6px; cursor:pointer; font-size:1rem; font-weight:600; align-self:flex-start;">
|
|
|
|
|
|
Save to AtoCore
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
<div id="cap-status" style="margin-top:1rem; font-size:0.9rem; min-height:1.5em;"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
document.getElementById('capture-form').addEventListener('submit', async (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
const prompt = document.getElementById('cap-prompt').value.trim();
|
|
|
|
|
|
const response = document.getElementById('cap-response').value.trim();
|
|
|
|
|
|
const project = document.getElementById('cap-project').value.trim();
|
|
|
|
|
|
const source = document.getElementById('cap-source').value;
|
|
|
|
|
|
const status = document.getElementById('cap-status');
|
|
|
|
|
|
if (!prompt || !response) { status.textContent = 'Need both prompt and response.'; return; }
|
|
|
|
|
|
status.textContent = 'Saving…';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch('/interactions', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {'Content-Type': 'application/json'},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
prompt: prompt, response: response,
|
|
|
|
|
|
client: source, project: project, reinforce: true
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
if (r.ok) {
|
|
|
|
|
|
const data = await r.json();
|
|
|
|
|
|
status.innerHTML = '✅ Saved — interaction ' + (data.interaction_id || '?').slice(0,8) +
|
|
|
|
|
|
'. Runs through extraction + triage within the hour.<br>' +
|
|
|
|
|
|
'<a href="/interactions/' + (data.interaction_id || '') + '">view</a>';
|
|
|
|
|
|
document.getElementById('capture-form').reset();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
status.textContent = '❌ ' + r.status + ': ' + (await r.text()).slice(0, 200);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) { status.textContent = '❌ ' + err.message; }
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
""")
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
'<h2>How this works</h2>'
|
|
|
|
|
|
'<ul>'
|
|
|
|
|
|
'<li><strong>Claude Code</strong> → auto-captured via Stop hook</li>'
|
|
|
|
|
|
'<li><strong>OpenClaw</strong> → auto-captured + gets AtoCore context injected on prompt start (Phase 7I)</li>'
|
|
|
|
|
|
'<li><strong>Anything else</strong> (Claude Desktop, mobile, web, ChatGPT) → paste here</li>'
|
|
|
|
|
|
'</ul>'
|
|
|
|
|
|
'<p>The extractor is aggressive about capturing signal — don\'t hand-filter. '
|
|
|
|
|
|
'If the conversation had nothing durable, triage will auto-reject.</p>'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return render_html(
|
|
|
|
|
|
"Capture — AtoCore",
|
|
|
|
|
|
"\n".join(lines),
|
|
|
|
|
|
breadcrumbs=[("Wiki", "/wiki"), ("Capture", "")],
|
|
|
|
|
|
active_path="/wiki/capture",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
# Phase 7E — /wiki/memories/{id}: memory detail page
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_memory_detail(memory_id: str) -> str | None:
|
|
|
|
|
|
"""Full view of a single memory: content, audit trail, source refs,
|
|
|
|
|
|
neighbors, graduation status. Fills the drill-down gap the list
|
|
|
|
|
|
views can't."""
|
|
|
|
|
|
from atocore.memory.service import get_memory_audit
|
|
|
|
|
|
from atocore.models.database import get_connection
|
|
|
|
|
|
|
|
|
|
|
|
with get_connection() as conn:
|
|
|
|
|
|
row = conn.execute("SELECT * FROM memories WHERE id = ?", (memory_id,)).fetchone()
|
|
|
|
|
|
if row is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
import json as _json
|
|
|
|
|
|
mem = dict(row)
|
|
|
|
|
|
try:
|
|
|
|
|
|
tags = _json.loads(mem.get("domain_tags") or "[]") or []
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
tags = []
|
|
|
|
|
|
|
|
|
|
|
|
lines = [f'<h1>{mem["memory_type"]}: <span style="color:var(--text);">{mem["content"][:80]}</span></h1>']
|
|
|
|
|
|
if len(mem["content"]) > 80:
|
|
|
|
|
|
lines.append(f'<blockquote><p>{mem["content"]}</p></blockquote>')
|
|
|
|
|
|
|
|
|
|
|
|
# Metadata row
|
|
|
|
|
|
meta_items = [
|
|
|
|
|
|
f'<span class="tag">{mem["status"]}</span>',
|
|
|
|
|
|
f'<strong>{mem["memory_type"]}</strong>',
|
|
|
|
|
|
]
|
|
|
|
|
|
if mem.get("project"):
|
|
|
|
|
|
meta_items.append(f'<a href="/wiki/projects/{mem["project"]}">{mem["project"]}</a>')
|
|
|
|
|
|
meta_items.append(f'confidence: <strong>{float(mem.get("confidence") or 0):.2f}</strong>')
|
|
|
|
|
|
meta_items.append(f'refs: <strong>{int(mem.get("reference_count") or 0)}</strong>')
|
|
|
|
|
|
if mem.get("valid_until"):
|
|
|
|
|
|
meta_items.append(f'<span class="mem-expiry">valid until {str(mem["valid_until"])[:10]}</span>')
|
|
|
|
|
|
lines.append(f'<p>{" · ".join(meta_items)}</p>')
|
|
|
|
|
|
|
|
|
|
|
|
if tags:
|
|
|
|
|
|
tag_links = " ".join(f'<a href="/wiki/domains/{t}" class="tag-badge">{t}</a>' for t in tags)
|
|
|
|
|
|
lines.append(f'<p><span class="mem-tags">{tag_links}</span></p>')
|
|
|
|
|
|
|
|
|
|
|
|
lines.append(f'<p class="meta">id: <code>{mem["id"]}</code> · created: {mem["created_at"]}'
|
|
|
|
|
|
f' · updated: {mem.get("updated_at", "?")}'
|
|
|
|
|
|
+ (f' · last referenced: {mem["last_referenced_at"]}' if mem.get("last_referenced_at") else '')
|
|
|
|
|
|
+ '</p>')
|
|
|
|
|
|
|
|
|
|
|
|
# Graduation
|
|
|
|
|
|
if mem.get("graduated_to_entity_id"):
|
|
|
|
|
|
eid = mem["graduated_to_entity_id"]
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
f'<h2>🎓 Graduated</h2>'
|
|
|
|
|
|
f'<p>This memory was promoted to a typed entity: '
|
|
|
|
|
|
f'<a href="/wiki/entities/{eid}">{eid[:8]}</a></p>'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Source chunk
|
|
|
|
|
|
if mem.get("source_chunk_id"):
|
|
|
|
|
|
lines.append(f'<h2>Source chunk</h2><p><code>{mem["source_chunk_id"]}</code></p>')
|
|
|
|
|
|
|
|
|
|
|
|
# Audit trail
|
|
|
|
|
|
audit = get_memory_audit(memory_id, limit=50)
|
|
|
|
|
|
if audit:
|
|
|
|
|
|
lines.append(f'<h2>Audit trail ({len(audit)} events)</h2><ul>')
|
|
|
|
|
|
for a in audit:
|
|
|
|
|
|
note = f' — {a["note"]}' if a.get("note") else ""
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
f'<li><code>{a["timestamp"]}</code> '
|
|
|
|
|
|
f'<strong>{a["action"]}</strong> '
|
|
|
|
|
|
f'<em>{a["actor"]}</em>{note}</li>'
|
|
|
|
|
|
)
|
|
|
|
|
|
lines.append('</ul>')
|
|
|
|
|
|
|
|
|
|
|
|
# Neighbors by shared tag
|
|
|
|
|
|
if tags:
|
|
|
|
|
|
from atocore.memory.service import get_memories as _get_memories
|
|
|
|
|
|
neighbors = []
|
|
|
|
|
|
for t in tags[:3]:
|
|
|
|
|
|
for other in _get_memories(active_only=True, limit=30):
|
|
|
|
|
|
if other.id == memory_id:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if any(ot == t for ot in (other.domain_tags or [])):
|
|
|
|
|
|
neighbors.append(other)
|
|
|
|
|
|
# Dedupe
|
|
|
|
|
|
seen = set()
|
|
|
|
|
|
uniq = []
|
|
|
|
|
|
for n in neighbors:
|
|
|
|
|
|
if n.id in seen:
|
|
|
|
|
|
continue
|
|
|
|
|
|
seen.add(n.id)
|
|
|
|
|
|
uniq.append(n)
|
|
|
|
|
|
if uniq:
|
|
|
|
|
|
lines.append(f'<h2>Related (by tag)</h2><ul>')
|
|
|
|
|
|
for n in uniq[:10]:
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
f'<li><a href="/wiki/memories/{n.id}">[{n.memory_type}] '
|
|
|
|
|
|
f'{n.content[:120]}</a>'
|
|
|
|
|
|
+ (f' <span class="tag">{n.project}</span>' if n.project else '')
|
|
|
|
|
|
+ '</li>'
|
|
|
|
|
|
)
|
|
|
|
|
|
lines.append('</ul>')
|
|
|
|
|
|
|
|
|
|
|
|
return render_html(
|
|
|
|
|
|
f"Memory {memory_id[:8]}",
|
|
|
|
|
|
"\n".join(lines),
|
|
|
|
|
|
breadcrumbs=[("Wiki", "/wiki"), ("Memory", "")],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
# Phase 7F — /wiki/domains/{tag}: cross-project domain view
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_domain(tag: str) -> str:
|
|
|
|
|
|
"""All memories + entities carrying a given domain_tag, grouped by project.
|
|
|
|
|
|
Answers 'what does the brain know about optics, across all projects?'"""
|
|
|
|
|
|
tag = (tag or "").strip().lower()
|
|
|
|
|
|
if not tag:
|
|
|
|
|
|
return render_html("Domain", "<p>No tag specified.</p>",
|
|
|
|
|
|
breadcrumbs=[("Wiki", "/wiki"), ("Domains", "")])
|
|
|
|
|
|
|
|
|
|
|
|
all_mems = get_memories(active_only=True, limit=500)
|
|
|
|
|
|
matching = [m for m in all_mems
|
|
|
|
|
|
if any((t or "").lower() == tag for t in (m.domain_tags or []))]
|
|
|
|
|
|
|
|
|
|
|
|
# Group by project
|
|
|
|
|
|
by_project: dict[str, list] = {}
|
|
|
|
|
|
for m in matching:
|
|
|
|
|
|
by_project.setdefault(m.project or "(global)", []).append(m)
|
|
|
|
|
|
|
|
|
|
|
|
lines = [f'<h1>Domain: <code>{tag}</code></h1>']
|
|
|
|
|
|
lines.append(f'<p class="meta">{len(matching)} active memories across {len(by_project)} projects</p>')
|
|
|
|
|
|
|
|
|
|
|
|
if not matching:
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
f'<p>No memories currently carry the tag <code>{tag}</code>.</p>'
|
|
|
|
|
|
'<p>Domain tags are assigned by the extractor when it identifies '
|
|
|
|
|
|
'the topical scope of a memory. They update over time.</p>'
|
|
|
|
|
|
)
|
|
|
|
|
|
return render_html(
|
|
|
|
|
|
f"Domain: {tag}",
|
|
|
|
|
|
"\n".join(lines),
|
|
|
|
|
|
breadcrumbs=[("Wiki", "/wiki"), ("Domains", ""), (tag, "")],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Sort projects by count descending, (global) last
|
|
|
|
|
|
def sort_key(item: tuple[str, list]) -> tuple[int, int]:
|
|
|
|
|
|
proj, mems = item
|
|
|
|
|
|
return (1 if proj == "(global)" else 0, -len(mems))
|
|
|
|
|
|
|
|
|
|
|
|
for proj, mems in sorted(by_project.items(), key=sort_key):
|
|
|
|
|
|
proj_link = proj if proj == "(global)" else f'<a href="/wiki/projects/{proj}">{proj}</a>'
|
|
|
|
|
|
lines.append(f'<h2>{proj_link} ({len(mems)})</h2><ul>')
|
|
|
|
|
|
for m in mems:
|
|
|
|
|
|
other_tags = [t for t in (m.domain_tags or []) if t != tag][:3]
|
|
|
|
|
|
other_tags_html = ""
|
|
|
|
|
|
if other_tags:
|
|
|
|
|
|
other_tags_html = ' <span class="mem-tags">' + " ".join(
|
|
|
|
|
|
f'<a href="/wiki/domains/{t}" class="tag-badge">{t}</a>' for t in other_tags
|
|
|
|
|
|
) + '</span>'
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
f'<li><a href="/wiki/memories/{m.id}">[{m.memory_type}] '
|
|
|
|
|
|
f'{m.content[:200]}</a>'
|
|
|
|
|
|
f' <span class="meta">conf {m.confidence:.2f} · refs {m.reference_count}</span>'
|
|
|
|
|
|
f'{other_tags_html}</li>'
|
|
|
|
|
|
)
|
|
|
|
|
|
lines.append('</ul>')
|
|
|
|
|
|
|
|
|
|
|
|
# Entities with this tag (if any have tags — currently they might not)
|
|
|
|
|
|
try:
|
|
|
|
|
|
all_entities = get_entities(limit=500)
|
|
|
|
|
|
ent_matching = []
|
|
|
|
|
|
for e in all_entities:
|
|
|
|
|
|
tags = e.properties.get("domain_tags") if e.properties else []
|
|
|
|
|
|
if isinstance(tags, list) and tag in [str(t).lower() for t in tags]:
|
|
|
|
|
|
ent_matching.append(e)
|
|
|
|
|
|
if ent_matching:
|
|
|
|
|
|
lines.append(f'<h2>🔧 Entities ({len(ent_matching)})</h2><ul>')
|
|
|
|
|
|
for e in ent_matching:
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
f'<li><a href="/wiki/entities/{e.id}">[{e.entity_type}] {e.name}</a>'
|
|
|
|
|
|
+ (f' <span class="tag">{e.project}</span>' if e.project else '')
|
|
|
|
|
|
+ '</li>'
|
|
|
|
|
|
)
|
|
|
|
|
|
lines.append('</ul>')
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
return render_html(
|
|
|
|
|
|
f"Domain: {tag}",
|
|
|
|
|
|
"\n".join(lines),
|
|
|
|
|
|
breadcrumbs=[("Wiki", "/wiki"), ("Domains", ""), (tag, "")],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
# /wiki/activity — autonomous-activity feed
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_activity(hours: int = 48, limit: int = 100) -> str:
|
|
|
|
|
|
"""Timeline of what the autonomous pipeline did recently. Answers
|
|
|
|
|
|
'what has the brain been doing while I was away?'"""
|
|
|
|
|
|
from atocore.memory.service import get_recent_audit
|
|
|
|
|
|
|
|
|
|
|
|
audit = get_recent_audit(limit=limit)
|
|
|
|
|
|
|
|
|
|
|
|
# Group events by category for summary
|
|
|
|
|
|
by_action: dict[str, int] = {}
|
|
|
|
|
|
by_actor: dict[str, int] = {}
|
|
|
|
|
|
for a in audit:
|
|
|
|
|
|
by_action[a["action"]] = by_action.get(a["action"], 0) + 1
|
|
|
|
|
|
by_actor[a["actor"]] = by_actor.get(a["actor"], 0) + 1
|
|
|
|
|
|
|
|
|
|
|
|
lines = [f'<h1>📡 Activity Feed</h1>']
|
|
|
|
|
|
lines.append(f'<p class="meta">Last {len(audit)} events in the memory audit log</p>')
|
|
|
|
|
|
|
|
|
|
|
|
# Summary chips
|
|
|
|
|
|
if by_action or by_actor:
|
|
|
|
|
|
lines.append('<h2>Summary</h2>')
|
|
|
|
|
|
lines.append('<p><strong>By action:</strong> ' +
|
|
|
|
|
|
" · ".join(f'{k}: {v}' for k, v in sorted(by_action.items(), key=lambda x: -x[1])) +
|
|
|
|
|
|
'</p>')
|
|
|
|
|
|
lines.append('<p><strong>By actor:</strong> ' +
|
|
|
|
|
|
" · ".join(f'<code>{k}</code>: {v}' for k, v in sorted(by_actor.items(), key=lambda x: -x[1])) +
|
|
|
|
|
|
'</p>')
|
|
|
|
|
|
|
|
|
|
|
|
# Action-type color/emoji
|
|
|
|
|
|
action_emoji = {
|
|
|
|
|
|
"created": "➕", "promoted": "✅", "rejected": "❌", "invalidated": "🚫",
|
|
|
|
|
|
"superseded": "🔀", "reinforced": "🔁", "updated": "✏️",
|
|
|
|
|
|
"auto_promoted": "⚡", "created_via_merge": "🔗",
|
|
|
|
|
|
"valid_until_extended": "⏳", "tag_canonicalized": "🏷️",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lines.append('<h2>Timeline</h2><ul>')
|
|
|
|
|
|
for a in audit:
|
|
|
|
|
|
emoji = action_emoji.get(a["action"], "•")
|
|
|
|
|
|
preview = a.get("content_preview") or ""
|
|
|
|
|
|
ts_short = a["timestamp"][:16] if a.get("timestamp") else "?"
|
|
|
|
|
|
mid_short = (a.get("memory_id") or "")[:8]
|
|
|
|
|
|
note = f' — <em>{a["note"]}</em>' if a.get("note") else ""
|
|
|
|
|
|
lines.append(
|
|
|
|
|
|
f'<li>{emoji} <code>{ts_short}</code> '
|
|
|
|
|
|
f'<strong>{a["action"]}</strong> '
|
|
|
|
|
|
f'<em>{a["actor"]}</em> '
|
|
|
|
|
|
f'<a href="/wiki/memories/{a["memory_id"]}">{mid_short}</a>'
|
|
|
|
|
|
f'{note}'
|
|
|
|
|
|
+ (f'<br><span style="opacity:0.6; font-size:0.85rem; margin-left:1.5rem;">{preview[:140]}</span>' if preview else '')
|
|
|
|
|
|
+ '</li>'
|
|
|
|
|
|
)
|
|
|
|
|
|
lines.append('</ul>')
|
|
|
|
|
|
|
|
|
|
|
|
return render_html(
|
|
|
|
|
|
"Activity — AtoCore",
|
|
|
|
|
|
"\n".join(lines),
|
|
|
|
|
|
breadcrumbs=[("Wiki", "/wiki"), ("Activity", "")],
|
|
|
|
|
|
active_path="/wiki/activity",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-13 16:09:12 -04:00
|
|
|
|
_TEMPLATE = """<!DOCTYPE html>
|
|
|
|
|
|
<html lang="en">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
|
|
<title>{{title}} — AtoCore</title>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
:root { --bg: #fafafa; --text: #1a1a2e; --accent: #2563eb; --border: #e2e8f0; --card: #fff; --hover: #f1f5f9; }
|
|
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
|
|
|
|
:root { --bg: #0f172a; --text: #e2e8f0; --accent: #60a5fa; --border: #334155; --card: #1e293b; --hover: #334155; }
|
|
|
|
|
|
}
|
|
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
|
|
body {
|
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
|
|
|
line-height: 1.7; color: var(--text); background: var(--bg);
|
|
|
|
|
|
max-width: 800px; margin: 0 auto; padding: 1.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
h1 { font-size: 1.8rem; margin-bottom: 0.5rem; color: var(--accent); }
|
|
|
|
|
|
h2 { font-size: 1.3rem; margin-top: 2rem; margin-bottom: 0.6rem; padding-bottom: 0.2rem; border-bottom: 2px solid var(--border); }
|
|
|
|
|
|
h3 { font-size: 1.1rem; margin-top: 1.2rem; margin-bottom: 0.4rem; }
|
|
|
|
|
|
p { margin-bottom: 0.8rem; }
|
|
|
|
|
|
ul { margin-left: 1.5rem; margin-bottom: 1rem; }
|
|
|
|
|
|
li { margin-bottom: 0.3rem; }
|
|
|
|
|
|
li ul { margin-top: 0.2rem; }
|
|
|
|
|
|
strong { color: var(--accent); font-weight: 600; }
|
|
|
|
|
|
em { opacity: 0.7; font-size: 0.9em; }
|
|
|
|
|
|
a { color: var(--accent); text-decoration: none; }
|
|
|
|
|
|
a:hover { text-decoration: underline; }
|
|
|
|
|
|
blockquote {
|
|
|
|
|
|
background: var(--card); border-left: 4px solid var(--accent);
|
|
|
|
|
|
padding: 0.6rem 1rem; margin: 1rem 0; border-radius: 0 6px 6px 0;
|
|
|
|
|
|
font-size: 0.9em;
|
|
|
|
|
|
}
|
|
|
|
|
|
hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
|
|
|
|
|
|
.breadcrumbs { margin-bottom: 1.5rem; font-size: 0.85em; opacity: 0.7; }
|
|
|
|
|
|
.breadcrumbs a { opacity: 0.8; }
|
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav)
Closes three gaps the user surfaced: (1) OpenClaw agents run blind
without AtoCore context, (2) mobile/desktop chats can't be captured
at all, (3) wiki UI hadn't kept up with backend capabilities.
Phase 7I — OpenClaw two-way bridge
- Plugin now calls /context/build on before_agent_start and prepends
the context pack to event.prompt, so whatever LLM runs underneath
(sonnet, opus, codex, local model) answers grounded in AtoCore
knowledge. Captured prompt stays the user's original text; fail-open
with a 5s timeout. Config-gated via injectContext flag.
- Plugin version 0.0.0 → 0.2.0; README rewritten.
UI refresh
- /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile
/ ChatGPT / other. Goes through normal /interactions pipeline with
client="claude-desktop|claude-web|claude-mobile|chatgpt|other".
Fixes the rotovap/mushroom-on-phone gap.
- /wiki/memories/{id} (Phase 7E) — full memory detail: content, status,
confidence, refs, valid_until, domain_tags (clickable to domain
pages), project link, source chunk, graduated-to-entity link, full
audit trail, related-by-tag neighbors.
- /wiki/domains/{tag} (Phase 7F) — cross-project view: all active
memories with the given tag grouped by project, sorted by count.
Case-insensitive, whitespace-tolerant. Also surfaces graduated
entities carrying the tag.
- /wiki/activity — autonomous-activity timeline feed. Summary chips
by action (created/promoted/merged/superseded/decayed/canonicalized)
and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay,
phase10-auto-promote, transient-to-durable, tag-canon, human-triage).
Answers "what has the brain been doing while I was away?"
- Home refresh: persistent topnav (Home · Activity · Capture · Triage
· Dashboard), "What the brain is doing" snippet above project cards
showing recent autonomous-actor counts, link to full activity.
Tests: +10 (capture page, memory detail + 404, domain cross-project +
empty + tag normalization, activity feed + groupings, home topnav,
superseded-source detail after merge). 440 → 450.
Known next: capture-browser extension for Claude.ai web (bigger
project, deferred); voice/mobile relay (adjacent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
|
|
|
|
.topnav { display: flex; gap: 0.25rem; flex-wrap: wrap; margin-bottom: 1rem; padding-bottom: 0.8rem; border-bottom: 1px solid var(--border); }
|
|
|
|
|
|
.topnav-item { padding: 0.35rem 0.8rem; background: var(--card); border: 1px solid var(--border); border-radius: 6px; font-size: 0.88rem; color: var(--text); opacity: 0.75; text-decoration: none; }
|
|
|
|
|
|
.topnav-item:hover { opacity: 1; background: var(--hover); text-decoration: none; }
|
|
|
|
|
|
.topnav-item.active { background: var(--accent); color: white; border-color: var(--accent); opacity: 1; }
|
|
|
|
|
|
.topnav-item.active:hover { background: var(--accent); }
|
|
|
|
|
|
.activity-snippet { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; margin: 1rem 0; }
|
|
|
|
|
|
.activity-snippet h3 { color: var(--accent); margin-bottom: 0.4rem; }
|
|
|
|
|
|
.activity-snippet ul { margin: 0.3rem 0 0 1.2rem; font-size: 0.9rem; }
|
|
|
|
|
|
.activity-snippet li { margin-bottom: 0.2rem; }
|
|
|
|
|
|
.stat-row { display: flex; gap: 1rem; flex-wrap: wrap; font-size: 0.9rem; margin: 0.4rem 0; }
|
|
|
|
|
|
.stat-row span { padding: 0.1rem 0.4rem; background: var(--hover); border-radius: 4px; }
|
2026-04-13 16:09:12 -04:00
|
|
|
|
.meta { font-size: 0.8em; opacity: 0.5; margin-top: 0.5rem; }
|
feat(assets): binary asset store + artifact entity + wiki evidence (Issue F)
Wires visual evidence into the knowledge graph. Images, PDFs, and CAD
exports can now be uploaded, deduped by SHA-256, thumbnailed, linked to
entities via EVIDENCED_BY, and rendered inline on wiki pages. Unblocks
AKC uploading voice-session screenshots alongside extracted entities.
- assets/ module: store_asset (hash dedup + MIME allowlist + 20 MB cap),
get_asset_binary, get_thumbnail (Pillow, on-disk cache under
.thumbnails/<size>/), list_orphan_assets, invalidate_asset
- models/database.py: new `assets` table + indexes
- engineering/service.py: `artifact` added to ENTITY_TYPES
- api/routes.py: POST /assets (multipart), GET /assets/{id},
/assets/{id}/thumbnail, /assets/{id}/meta, /admin/assets/orphans,
DELETE /assets/{id} (409 if still referenced),
GET /entities/{id}/evidence (EVIDENCED_BY artifacts with asset meta)
- main.py: all new paths aliased under /v1
- engineering/wiki.py: entity pages render EVIDENCED_BY → artifact as a
"Visual evidence" thumbnail strip; artifact pages render the full
image + caption + capture_context
- deploy/dalidou/docker-compose.yml: bind-mount ${ATOCORE_ASSETS_DIR}
- config.py: assets_dir + assets_max_upload_bytes settings
- requirements.txt + pyproject.toml: python-multipart, Pillow>=10.0.0
- tests/test_assets.py: 16 tests (dedup, cap, thumbnail cache, orphan
detection, invalidate gating, API upload/fetch, evidence, v1 aliases,
wiki rendering)
- DEV-LEDGER.md: session log + cleanup note + test_count 478 -> 494
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:46:52 -04:00
|
|
|
|
.evidence-strip { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 0.75rem 0 1.25rem; }
|
|
|
|
|
|
.evidence-tile { margin: 0; background: var(--card); border: 1px solid var(--border); border-radius: 6px; padding: 0.4rem; max-width: 270px; }
|
|
|
|
|
|
.evidence-tile img { display: block; max-width: 100%; height: auto; border-radius: 3px; }
|
|
|
|
|
|
.evidence-tile figcaption { font-size: 0.8rem; margin-top: 0.35rem; opacity: 0.85; }
|
|
|
|
|
|
.evidence-pdf, .evidence-other { padding: 0.6rem 0.8rem; font-size: 0.9rem; }
|
|
|
|
|
|
.artifact-full figure, .artifact-full { margin: 0 0 1rem; }
|
|
|
|
|
|
.artifact-full img { display: block; max-width: 100%; height: auto; border: 1px solid var(--border); border-radius: 4px; }
|
|
|
|
|
|
.artifact-full figcaption { font-size: 0.9rem; margin-top: 0.5rem; opacity: 0.85; }
|
2026-04-13 16:09:12 -04:00
|
|
|
|
.tag { background: var(--accent); color: var(--bg); padding: 0.1rem 0.4rem; border-radius: 3px; font-size: 0.75em; margin-left: 0.3rem; }
|
|
|
|
|
|
.search-box { display: flex; gap: 0.5rem; margin: 1.5rem 0; }
|
|
|
|
|
|
.search-box input {
|
|
|
|
|
|
flex: 1; padding: 0.6rem 1rem; border: 2px solid var(--border);
|
|
|
|
|
|
border-radius: 8px; background: var(--card); color: var(--text);
|
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.search-box input:focus { border-color: var(--accent); outline: none; }
|
|
|
|
|
|
.search-box button {
|
|
|
|
|
|
padding: 0.6rem 1.2rem; background: var(--accent); color: var(--bg);
|
|
|
|
|
|
border: none; border-radius: 8px; cursor: pointer; font-size: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.card-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; margin: 1rem 0; }
|
|
|
|
|
|
@media (min-width: 600px) { .card-grid { grid-template-columns: 1fr 1fr; } }
|
|
|
|
|
|
.card {
|
|
|
|
|
|
display: block; background: var(--card); border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 10px; padding: 1.2rem; text-decoration: none;
|
|
|
|
|
|
color: var(--text); transition: border-color 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.card:hover { border-color: var(--accent); background: var(--hover); text-decoration: none; }
|
|
|
|
|
|
.card h3 { color: var(--accent); margin: 0 0 0.3rem 0; }
|
|
|
|
|
|
.card p { font-size: 0.9em; margin: 0; opacity: 0.8; }
|
|
|
|
|
|
.card .stats { font-size: 0.8em; margin-top: 0.5rem; opacity: 0.5; }
|
2026-04-13 18:47:44 -04:00
|
|
|
|
.card .client { font-size: 0.85em; opacity: 0.65; margin-bottom: 0.3rem; font-style: italic; }
|
|
|
|
|
|
.card h3 .tag { font-size: 0.65em; vertical-align: middle; margin-left: 0.4rem; }
|
2026-04-16 20:28:56 -04:00
|
|
|
|
.triage-notice { background: var(--card); border-left: 4px solid var(--accent); padding: 0.6rem 1rem; border-radius: 4px; margin: 0.8rem 0; }
|
|
|
|
|
|
.triage-warning { background: #fef3c7; color: #78350f; border-left: 4px solid #d97706; padding: 0.6rem 1rem; border-radius: 4px; margin: 0.8rem 0; }
|
|
|
|
|
|
@media (prefers-color-scheme: dark) { .triage-warning { background: #451a03; color: #fde68a; } }
|
feat: Phase 3 V1 — Auto-Organization (domain_tags + valid_until)
Adds structural metadata that the LLM triage was already implicitly
reasoning about ("stale snapshot" → reject). Phase 3 captures that
reasoning as fields so it can DRIVE retrieval, not just rejection.
Schema (src/atocore/models/database.py):
- domain_tags TEXT DEFAULT '[]' JSON array of lowercase topic keywords
- valid_until DATETIME ISO date; null = permanent
- idx_memories_valid_until index for efficient expiry queries
Memory service (src/atocore/memory/service.py):
- Memory dataclass gains domain_tags + valid_until
- create_memory, update_memory accept/persist both
- _row_to_memory safely reads both (JSON-decode + null handling)
- _normalize_tags helper: lowercase, dedup, strip, cap at 10
- get_memories_for_context filters expired (valid_until < today UTC)
- _rank_memories_for_query adds tag-boost: memories whose domain_tags
appear as substrings in query text rank higher (tertiary key after
content-overlap density + absolute overlap, before confidence)
LLM extractor (_llm_prompt.py → llm-0.5.0):
- SYSTEM_PROMPT documents domain_tags (2-5 keywords) + valid_until
(time-bounded facts get expiry dates; durable facts stay null)
- normalize_candidate_item parses both fields from model output with
graceful fallback for string/null/missing
LLM triage (scripts/auto_triage.py):
- TRIAGE_SYSTEM_PROMPT documents same two fields
- parse_verdict extracts them from verdict JSON
- On promote: PUT /memory/{id} with tags + valid_until BEFORE
POST /memory/{id}/promote, so active memories carry them
API (src/atocore/api/routes.py):
- MemoryCreateRequest: adds domain_tags, valid_until
- MemoryUpdateRequest: adds domain_tags, valid_until, memory_type
- GET /memory response exposes domain_tags + valid_until + created_at
Triage UI (src/atocore/engineering/triage_ui.py):
- Renders existing tags as colored badges
- Adds inline text field for tags (comma-separated) + date picker for
valid_until on every candidate card
- Save&Promote button persists edits via PUT then promotes
- Plain Promote (and Y shortcut) also saves tags/expiry if edited
Wiki (src/atocore/engineering/wiki.py):
- Search now matches memory content OR domain_tags
- Search results render tags as clickable badges linking to
/wiki/search?q=<tag> for cross-project navigation
- valid_until shown as amber "valid until YYYY-MM-DD" hint
Tests: 303 → 308 (5 new for Phase 3 behavior):
- test_create_memory_with_tags_and_valid_until
- test_create_memory_normalizes_tags
- test_update_memory_sets_tags_and_valid_until
- test_get_memories_for_context_excludes_expired
- test_context_builder_tag_boost_orders_results
Deferred (explicitly): temporal_scope enum, source_refs memory graph,
HDBSCAN clustering, memory detail wiki page, backfill of existing
actives. See docs/MASTER-BRAIN-PLAN.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:37:01 -04:00
|
|
|
|
.mem-tags { display: inline-flex; gap: 0.25rem; flex-wrap: wrap; vertical-align: middle; }
|
|
|
|
|
|
.tag-badge { background: var(--accent); color: white; padding: 0.1rem 0.5rem; border-radius: 10px; font-size: 0.7rem; font-family: monospace; text-decoration: none; font-weight: 500; }
|
|
|
|
|
|
.tag-badge:hover { opacity: 0.85; text-decoration: none; }
|
|
|
|
|
|
.mem-expiry { font-size: 0.75rem; color: #d97706; font-style: italic; margin-left: 0.4rem; }
|
|
|
|
|
|
@media (prefers-color-scheme: dark) { .mem-expiry { color: #fbbf24; } }
|
feat: Phase 6 — Living Taxonomy + Universal Capture
Closes two real-use gaps:
1. "APM tool" gap: work done outside Claude Code (desktop, web, phone,
other machine) was invisible to AtoCore.
2. Project discovery gap: manual JSON-file edits required to promote
an emerging theme to a first-class project.
B — atocore_remember MCP tool (scripts/atocore_mcp.py):
- New MCP tool for universal capture from any MCP-aware client
(Claude Desktop, Code, Cursor, Zed, Windsurf, etc.)
- Accepts content (required) + memory_type/project/confidence/
valid_until/domain_tags (all optional with sensible defaults)
- Creates a candidate memory, goes through the existing 3-tier triage
(no bypass — the quality gate catches noise)
- Detailed tool description guides Claude on when to invoke: "remember
this", "save that for later", "don't lose this fact"
- Total tools exposed by MCP server: 14 → 15
C.1 Emerging-concepts detector (scripts/detect_emerging.py):
- Nightly scan of active + candidate memories for:
* Unregistered project names with ≥3 memory occurrences
* Top 20 domain_tags by frequency (emerging categories)
* Active memories with reference_count ≥ 5 + valid_until set
(reinforced transients — candidates for extension)
- Writes findings to atocore/proposals/* project state entries
- Emits "warning" alert via Phase 4 framework the FIRST time a new
project crosses the 5-memory alert threshold (avoids spam)
- Configurable via env vars: ATOCORE_EMERGING_PROJECT_MIN (default 3),
ATOCORE_EMERGING_ALERT_THRESHOLD (default 5), TOP_TAGS_LIMIT (20)
C.2 Registration surface (src/atocore/api/routes.py + wiki.py):
- POST /admin/projects/register-emerging — one-click register with
sensible defaults (ingest_roots auto-filled with
vault:incoming/projects/<id>/ convention). Clears the proposal
from the dashboard list on success.
- Dashboard /admin/dashboard: new "proposals" section with
unregistered_projects + emerging_categories + reinforced_transients.
- Wiki homepage: "📋 Emerging" section rendering each unregistered
project as a card with count + 2 sample memory previews + inline
"📌 Register as project" button that calls the endpoint via fetch,
reloads the page on success.
C.3 Transient-to-durable extension
(src/atocore/memory/service.py + API + cron):
- New extend_reinforced_valid_until() function — scans active memories
with valid_until in the next 30 days and reference_count ≥ 5.
Extends expiry by 90 days. If reference_count ≥ 10, clears expiry
entirely (makes permanent). Writes audit rows via the Phase 4
memory_audit framework with actor="transient-to-durable".
- POST /admin/memory/extend-reinforced — API wrapper for cron.
- Matches the user's intuition: "something transient becomes important
if you keep coming back to it".
Nightly cron (deploy/dalidou/batch-extract.sh):
- Step F2: detect_emerging.py (after F pipeline summary)
- Step F3: /admin/memory/extend-reinforced (before integrity check)
- Both fail-open; errors don't break the pipeline.
Tests: 366 → 374 (+8 for Phase 6):
- 6 tests for extend_reinforced_valid_until covering:
extension path, permanent path, skip far-future, skip low-refs,
skip permanent memories, audit row write
- 2 smoke tests for the detector (imports cleanly, handles empty DB)
- MCP tool changes don't need new tests — the wrapper is pure passthrough
Design decisions documented in plan file:
- atocore_remember deliberately doesn't bypass triage (quality gate)
- Detector is passive (surfaces proposals) not active (auto-registers)
- Sensible ingest-root defaults ("vault:incoming/projects/<id>/")
so registration is one-click with no file-path thinking
- Extension adds 90 days rather than clearing expiry (gradual
permanence earned through sustained reinforcement)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 08:08:55 -04:00
|
|
|
|
/* Phase 6 C.2 — Emerging projects section */
|
|
|
|
|
|
.emerging-intro { font-size: 0.9rem; opacity: 0.75; margin-bottom: 0.8rem; }
|
|
|
|
|
|
.emerging-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; margin-bottom: 1rem; }
|
|
|
|
|
|
.emerging-card { background: var(--card); border: 1px dashed var(--accent); border-radius: 8px; padding: 1rem; }
|
|
|
|
|
|
.emerging-card h3 { margin: 0 0 0.3rem 0; color: var(--accent); font-family: monospace; font-size: 1rem; }
|
|
|
|
|
|
.emerging-count { font-size: 0.8rem; opacity: 0.6; margin-bottom: 0.5rem; }
|
|
|
|
|
|
.emerging-samples { font-size: 0.85rem; margin: 0.5rem 0; padding-left: 1.2rem; opacity: 0.8; }
|
|
|
|
|
|
.emerging-samples li { margin-bottom: 0.25rem; }
|
|
|
|
|
|
.btn-register-emerging { width: 100%; padding: 0.45rem 0.9rem; background: var(--accent); color: white; border: 1px solid var(--accent); border-radius: 4px; cursor: pointer; font-size: 0.88rem; font-weight: 500; margin-top: 0.5rem; }
|
|
|
|
|
|
.btn-register-emerging:hover { opacity: 0.9; }
|
2026-04-13 16:09:12 -04:00
|
|
|
|
</style>
|
feat: Phase 6 — Living Taxonomy + Universal Capture
Closes two real-use gaps:
1. "APM tool" gap: work done outside Claude Code (desktop, web, phone,
other machine) was invisible to AtoCore.
2. Project discovery gap: manual JSON-file edits required to promote
an emerging theme to a first-class project.
B — atocore_remember MCP tool (scripts/atocore_mcp.py):
- New MCP tool for universal capture from any MCP-aware client
(Claude Desktop, Code, Cursor, Zed, Windsurf, etc.)
- Accepts content (required) + memory_type/project/confidence/
valid_until/domain_tags (all optional with sensible defaults)
- Creates a candidate memory, goes through the existing 3-tier triage
(no bypass — the quality gate catches noise)
- Detailed tool description guides Claude on when to invoke: "remember
this", "save that for later", "don't lose this fact"
- Total tools exposed by MCP server: 14 → 15
C.1 Emerging-concepts detector (scripts/detect_emerging.py):
- Nightly scan of active + candidate memories for:
* Unregistered project names with ≥3 memory occurrences
* Top 20 domain_tags by frequency (emerging categories)
* Active memories with reference_count ≥ 5 + valid_until set
(reinforced transients — candidates for extension)
- Writes findings to atocore/proposals/* project state entries
- Emits "warning" alert via Phase 4 framework the FIRST time a new
project crosses the 5-memory alert threshold (avoids spam)
- Configurable via env vars: ATOCORE_EMERGING_PROJECT_MIN (default 3),
ATOCORE_EMERGING_ALERT_THRESHOLD (default 5), TOP_TAGS_LIMIT (20)
C.2 Registration surface (src/atocore/api/routes.py + wiki.py):
- POST /admin/projects/register-emerging — one-click register with
sensible defaults (ingest_roots auto-filled with
vault:incoming/projects/<id>/ convention). Clears the proposal
from the dashboard list on success.
- Dashboard /admin/dashboard: new "proposals" section with
unregistered_projects + emerging_categories + reinforced_transients.
- Wiki homepage: "📋 Emerging" section rendering each unregistered
project as a card with count + 2 sample memory previews + inline
"📌 Register as project" button that calls the endpoint via fetch,
reloads the page on success.
C.3 Transient-to-durable extension
(src/atocore/memory/service.py + API + cron):
- New extend_reinforced_valid_until() function — scans active memories
with valid_until in the next 30 days and reference_count ≥ 5.
Extends expiry by 90 days. If reference_count ≥ 10, clears expiry
entirely (makes permanent). Writes audit rows via the Phase 4
memory_audit framework with actor="transient-to-durable".
- POST /admin/memory/extend-reinforced — API wrapper for cron.
- Matches the user's intuition: "something transient becomes important
if you keep coming back to it".
Nightly cron (deploy/dalidou/batch-extract.sh):
- Step F2: detect_emerging.py (after F pipeline summary)
- Step F3: /admin/memory/extend-reinforced (before integrity check)
- Both fail-open; errors don't break the pipeline.
Tests: 366 → 374 (+8 for Phase 6):
- 6 tests for extend_reinforced_valid_until covering:
extension path, permanent path, skip far-future, skip low-refs,
skip permanent memories, audit row write
- 2 smoke tests for the detector (imports cleanly, handles empty DB)
- MCP tool changes don't need new tests — the wrapper is pure passthrough
Design decisions documented in plan file:
- atocore_remember deliberately doesn't bypass triage (quality gate)
- Detector is passive (surfaces proposals) not active (auto-registers)
- Sensible ingest-root defaults ("vault:incoming/projects/<id>/")
so registration is one-click with no file-path thinking
- Extension adds 90 days rather than clearing expiry (gradual
permanence earned through sustained reinforcement)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 08:08:55 -04:00
|
|
|
|
<script>
|
|
|
|
|
|
async function registerEmerging(projectId) {
|
|
|
|
|
|
if (!confirm(`Register "${projectId}" as a first-class project?\n\nThis creates:\n• /wiki/projects/${projectId} page\n• System map + gaps + killer queries\n• Triage + graduation support\n\nIngest root defaults to vault:incoming/projects/${projectId}/`)) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch('/admin/projects/register-emerging', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {'Content-Type': 'application/json'},
|
|
|
|
|
|
body: JSON.stringify({project_id: projectId}),
|
|
|
|
|
|
});
|
|
|
|
|
|
if (r.ok) {
|
|
|
|
|
|
const data = await r.json();
|
|
|
|
|
|
alert(data.message || `Registered ${projectId}`);
|
|
|
|
|
|
window.location.reload();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const err = await r.text();
|
|
|
|
|
|
alert(`Registration failed: ${r.status}\n${err.substring(0, 300)}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
alert(`Network error: ${e.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
2026-04-13 16:09:12 -04:00
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
{{nav}}
|
|
|
|
|
|
{{body}}
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>"""
|