@aaarc/recursivelearnmcp
v0.1.3
Published
A recoverable recursive-learning tree with MCP, HTTP/SSE, and an embedded web UI.
Readme
@incursion/cli
The incursion command: new, ls, show, focus, rebuild, undo, orphans,
serve (embedded web UI + HTTP/SSE), mcp (the complete entrypoint — the same UI
and HTTP/SSE surface plus MCP over stdio), and registry (the deployable public-forest
HTTP gateway backed by built-in SQLite). Run incursion with no arguments (or
--help) for the full usage text.
The release tarball embeds apps/web under dist/web, so users do not need to install,
start, or deploy Next.js separately. Both long-running commands print their resolved
endpoints on startup. mcp prints them to stderr because stdout is reserved exclusively
for JSON-RPC.
The production runtime also opens one Automerge-backed history repository beside the
human-readable tree projection. Every real mutation is committed as a complete-forest
checkpoint before the projection and SSE notification are published. The embedded UI's
检查点 tab can restore any checkpoint append-only; a stale HEAD or manually modified
worktree is rejected instead of being silently overwritten.
Public forest deployment
incursion registry --port=7780 --db=/path/public-forest.sqlite is the self-contained
reference deployment. Public reads require no credential. Writes are disabled unless
INCURSION_REGISTRY_TOKEN is set, and then require Authorization: Bearer <token>.
The registry validates the narrow PublishableNode schema and computes its SHA-256
server-side, so attempts, notes, verification/mastery and caller-supplied hashes cannot
cross the publishing boundary. Local serve/mcp processes connect through
INCURSION_REMOTE_URL; that replaceable HTTP boundary allows a later PostgreSQL or
hosted-database implementation without changing MCP tools or mounted snapshot semantics.
incursion mcp: one owner, any number of Codex instances
@incursion/mcp's own bin.ts is deliberately stdio-only (see that package's README —
it exists for its own subprocess tests and for hosts that only want MCP). The task
brief's "MCP server 启动时默认同时起 HTTP" behavior lives here instead. incursion mcp
reuses or starts one detached owner for the selected forest. The owner alone opens the
Automerge writer, embedded UI, HTTP/SSE, and an authenticated local MCP-over-HTTP
endpoint. Each Codex process exposes stdio by proxying to that endpoint. Many Codex
windows therefore share one Engine; closing the first window does not kill the UI,
and no second process can overwrite the projection. A requested port collision falls
back to an OS-assigned free port recorded in runtime.json.
Because each proxy's stdout is the MCP JSON-RPC wire protocol, every informational message (including the owner PID and actual HTTP endpoint) goes to stderr, never stdout. Writing anything else to stdout would corrupt the host connection.
Color coding
show colors each rendered line by the node's most specific state: green for verified,
blue for asserted, yellow for provisional, gray for pruned; everything else is
printed uncolored (src/tree-view.ts). Two independent opt-outs are honored, checked in
src/color.ts:
NO_COLOR(https://no-color.org) — presence of the variable disables color regardless of its value, even on a real TTY.- Non-TTY stdout — anything piped, redirected, or captured by a test's
execFile/spawnauto-disables color. This isn't just a nicety: it's what makestest/cli.test.ts's output assertions byte-stable without stripping ANSI codes in every test.
Coloring is layered on top of core's own plain-text render() rather than folding color
into @incursion/core itself — core's render() output is also what MCP's tree_render
tool hands to a model, where ANSI escape codes would just be extra tokens to parse around
for no benefit.
A core gap found while building rebuild
rebuild [tree] is meant to verify that a tree's on-disk archive is recoverable purely
from (tree, nodes), by comparing every node's persisted rollup against what core's
rebuildAllRollups() recomputes from scratch. In practice this needed one carve-out:
recomputeRollup()'s composer always returns a string (defaultComposer("", []) is
"", not null), so it can never reproduce rollup: null — the value every node
actually starts at (algebra.ts's createTree) and keeps until the first close()
anywhere bubbles a value into it. A freshly created, never-closed tree's root would
therefore always show up as a false-positive "mismatch" (null on disk vs. ""
recomputed) if compared naively. rebuildAllRollups()'s own doc comment scopes its
invariant to "for any sequence of closes"; cmdRebuild (src/commands.ts) follows that
same scope and skips nodes whose on-disk rollup is still null, rather than silently
loosening the comparison or reporting a mismatch that core's own invariant never claimed
to cover.
Testing
Every test spawns the actual built dist/bin.js — via execFile for commands that exit
(test/cli.test.ts) and via spawn + a line-matching wait for the two long-running
commands, serve and mcp (test/long-running.test.ts, which kills the child process in
afterEach rather than waiting for it to exit on its own). No command is ever called
in-process through run() directly — the point is to exercise the real binary a user
would actually invoke, including argv parsing and process exit codes.
