feat: Claude Code context injection (UserPromptSubmit hook)

Closes the asymmetry the user surfaced: before this, Claude Code
captured every turn (Stop hook) but retrieval only happened when
Claude chose to call atocore_context (opt-in MCP tool). OpenClaw had
both sides covered after 7I; Claude Code did not.

Now symmetric. Every Claude Code prompt is auto-sent to
/context/build and the returned pack is prepended via
hookSpecificOutput.additionalContext — same as what OpenClaw's
before_agent_start hook now does.

- deploy/hooks/inject_context.py — UserPromptSubmit hook. Fail-open
  (always exit 0). Skips short/XML prompts. 5s timeout. Project
  inference mirrors capture_stop.py cwd→slug table. Kill switch:
  ATOCORE_CONTEXT_DISABLED=1.
- ~/.claude/settings.json registered the hook (local config, not
  committed; copy-paste snippet in docs/capture-surfaces.md).
- Removed /wiki/capture from topnav. Endpoint still exists but the
  page is now labeled "fallback only" with a warning banner. The
  sanctioned surfaces are Claude Code + OpenClaw; manual paste is
  explicitly not the design.
- docs/capture-surfaces.md — scope statement: two surfaces, nothing
  else. Anthropic API polling explicitly prohibited.

Tests: +8 for inject_context.py (exit 0 on all failure modes, kill
switch, short prompt filter, XML filter, bad stdin, mock-server
success shape, project inference from cwd). Updated 2 wiki tests
for the topnav change. 450 → 459.

Verified live with real AtoCore: injected 2979 chars of atocore
project context on a cwd-matched prompt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 12:01:41 -04:00
parent 6e43cc7383
commit 9c91d778d9
5 changed files with 448 additions and 16 deletions

View File

@@ -29,7 +29,6 @@ from atocore.projects.registry import load_project_registry
_TOP_NAV_LINKS = [
("🏠 Home", "/wiki"),
("📡 Activity", "/wiki/activity"),
("📥 Capture", "/wiki/capture"),
("🔀 Triage", "/admin/triage"),
("📊 Dashboard", "/admin/dashboard"),
]
@@ -337,16 +336,27 @@ def render_search(query: str) -> str:
# ---------------------------------------------------------------------
# Phase 7I follow-up — /wiki/capture: paste mobile/desktop chats
# /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.
# ---------------------------------------------------------------------
def render_capture() -> str:
lines = ['<h1>📥 Capture a conversation</h1>']
lines = ['<h1>📥 Manual capture (fallback only)</h1>']
lines.append(
'<p>Paste a chat from Claude Desktop, Claude.ai (web or mobile), '
'or any other LLM. It goes through the same pipeline as auto-captured '
'interactions: extraction → 3-tier triage → active memory if it carries signal.</p>'
'<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>'
)
lines.append('<p class="meta">Your prompt + the assistant\'s response. Project is optional — '
'the extractor infers it from content.</p>')