| name | runcor-lattice |
|---|---|
| description | Operate a running Runcor Lattice via its local bridge HTTP API. Use when the user wants to instantiate an autonomous engineering agent, hand it a job, watch progress, or query its trace. The lattice owns its own SQLite entity file; you talk to the bridge daemon on http://127.0.0.1:7100 (or operator-configured port). NEVER write to the source codebase the operator handed in — only via lattice tool_manifest capabilities, with operator-supplied paths. |
| allowed-tools | Bash, Read, Write, Edit, Glob, Grep |
You are operating the Runcor Lattice from a project's context. The lattice is a long-running autonomous cognitive entity (one SQLite file IS the entity); you are NOT the lattice — you are its operator, the way a senior engineer is operator of a junior who they hand work to and review.
The bridge is the HTTP API the lattice exposes. By default it
binds 127.0.0.1:7100. Operators sometimes override via
RUNCOR_BRIDGE_PORT.
curl -sS http://127.0.0.1:7100/api/health
# {"ok":true} → up
# refused / no response → ask the operator to boot it:
# cd ~/runcor-lattice && pnpm bridge:startIf the operator hasn't cloned the lattice yet, point them at
https://github.com/runcor-ai/lattice. Quickstart in that repo's
README is:
git clone https://github.com/runcor-ai/lattice ~/runcor-lattice
cd ~/runcor-lattice
pnpm install # native build scripts auto-approved via pnpm-workspace.yaml
pnpm build # ~50s on first run; cached after
pnpm bridge:build
pnpm bridge:start # boots on :7100 (or RUNCOR_BRIDGE_PORT)If the operator hits any of these on a fresh machine, here's what
to look at — these are real friction points caught during the
public-repo cold-clone smoke test. The current main has the
fixes; older clones may not.
| Symptom | Likely cause | Fix |
|---|---|---|
pnpm install warns ERR_PNPM_IGNORED_BUILDS for better-sqlite3, esbuild, or vue-demi |
The allowBuilds: map in pnpm-workspace.yaml is missing / has placeholder text values instead of true |
Set allowBuilds: { better-sqlite3: true, esbuild: true, vue-demi: true } in pnpm-workspace.yaml. Pull latest if you can; otherwise edit + pnpm install again. The pnpm.onlyBuiltDependencies key in package.json is NO LONGER read in pnpm 11 — don't put it there. |
Cannot find module 'better-sqlite3' or missing better_sqlite3.node at runtime |
Native binding never compiled (above bug, OR a stale install) | rm -rf node_modules && pnpm install after the allowBuilds: map is in place |
Bridge is up (/api/health returns ok) but /api/bundles returns [] |
The bridge resolved prebuilt/ from the wrong cwd |
Pull latest (the bridge now resolves it relative to its own module path), OR set RUNCOR_BRIDGE_PREBUILT=<repo-root>/prebuilt before pnpm bridge:start |
claude-code-host backend hangs / never returns on Windows |
Node spawn can't resolve the .cmd shim by bare name. The shipped spawnCliRunner uses shell: true on win32 — if you see this, you're on an older clone |
Pull latest. Or set the engine to use command: "claude.cmd" explicitly via the claude_code_host.config.command override. |
pnpm bridge:start exits with EADDRINUSE on :7100 |
A previous bridge is still running, OR another service uses 7100 | RUNCOR_BRIDGE_PORT=7110 pnpm bridge:start (or any free port) |
Health check after a clean boot SHOULD show:
curl -sS http://127.0.0.1:7100/api/health # {"ok":true}
curl -sS http://127.0.0.1:7100/api/bundles | jq length # 5
curl -sS http://127.0.0.1:7100/api/lattices # []If /api/bundles is 0, NOTHING ELSE WILL WORK with bundles —
instantiating from bundle_id will silently fail. Confirm 5
first.
The bridge has six operator-facing endpoint families:
| Endpoint | What |
|---|---|
GET /api/health |
Liveness check |
GET /api/lattices |
Roster of running lattices |
POST /api/lattices |
Instantiate a new lattice. Options: persona_bundles (ordered Layer-1 fragments), init_seed (Layer-2 setup, promoted to memory once), bundle_id, or resume_from_path to reopen an existing entity |
GET /api/lattices/:id |
Inspect (cycle, memory counts, identity, recent decisions, dial state, drift history) |
GET /api/lattices/:id/trace |
Paginated trace (filter by kind, phase, after_cycle) |
GET /api/lattices/:id/trace/stream |
SSE live trace (server-side coalesced) |
PATCH /api/lattices/:id/dials |
Adjust dials mid-flight (autonomy / etc) — requires why field |
POST /api/lattices/:id/actions/{pause,resume,stop,swap-backend} |
Lifecycle controls |
POST /api/lattices/:id/jobs |
Hand the lattice a job (title + why + body + items[]). A gated checklist-plan item is auto-inserted first; the job won't close until the lattice writes its plan |
POST /api/lattices/:id/jobs/:job_id/items |
Append one gated item to an open job (the lattice also does this itself mid-run) |
POST /api/lattices/:id/escalations/:escalation_id/decide |
Approve / reject substrate-escalated outputs |
GET /api/bundles |
List prebuilt role bundles available |
POST /api/companies |
Instantiate multiple lattices from bundles in one call (bundles with placeholder tool paths will reject) |
GET /api/secrets, POST /api/secrets |
Configure API keys for model backends |
The full schemas (zod-typed) are in the bridge-shared package; if
you need them, ask the operator to point you at
packages/bridge-shared/src/index.ts.
For a software-engineering task, you'll typically POST something
like this. Fill <...> with operator-supplied or operator-confirmed
values:
const res = await fetch('http://127.0.0.1:7100/api/lattices', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'descriptive-name',
identity_seed: '<seed prompt — what this lattice IS>', // or omit if bundle_id covers it
goals: ['<one-line goal>'],
bundle_id: 'software-engineer', // pulls identity + starting knowledge from prebuilt/software-engineer/
autonomy: 'medium', // 'low' | 'medium' | 'high'
dialecticDepth: 0, // 0 = single-model decider; ≥1 = dialectic with N coach rounds
model_backend: { kind: 'claude-code-host' }, // or { kind: 'direct-api', config: { provider: 'anthropic' } }
tool_manifest: [
// The lattice's tool surface. Each entry must specify a concrete config.
// PATH-JAIL DISCIPLINE: every path is absolute; the runtime realpaths it
// at construction and re-checks on every read/write so the lattice
// cannot escape via symlinks or '..'.
{
name: 'src-listing',
kind: 'fs-read',
role: { sense: true, action: false },
readOnly: true, destructive: false, concurrencySafe: true,
config: { root: '<absolute path to source repo>', maxEntries: 300 },
},
{
name: 'src-read',
kind: 'fs-read-content',
role: { sense: true, action: true },
readOnly: true, destructive: false, concurrencySafe: true,
config: { root: '<absolute path to source repo>', defaultMaxBytes: 16000, hardMaxBytes: 200000 },
},
{
name: 'out-write',
kind: 'fs-write',
role: { sense: false, action: true },
readOnly: false, destructive: false, concurrencySafe: false,
config: { outDir: '<absolute path to writable output dir — the lattice will auto-pair an fs-read sense over this dir>' },
},
{
name: 'src-shell',
kind: 'shell-exec',
role: { sense: false, action: true },
readOnly: false, destructive: false, concurrencySafe: false,
config: {
cwd: '<absolute path to source repo>',
timeoutMs: 30000, outputMaxBytes: 8000,
// Default allowlist is read-only verbs: grep/find/ls/cat/git/npm/node/pnpm/jq/tree etc.
// To allow writes / file ops / build commands, supply allowedVerbs explicitly.
},
},
{
name: 'delegate-cc',
kind: 'claude-delegate',
role: { sense: false, action: true },
readOnly: false, destructive: false, concurrencySafe: false,
config: {
workdir: '<absolute path — typically same as the writable output dir or port dir>',
timeoutMs: 600000, outputMaxBytes: 32000,
},
},
],
}),
});
const { lattice_id, sqlite_path, trace_stream_url } = await res.json();A job is what the operator hands the lattice. Title + why + items.
Each item has a completion_check — JSON describing how to verify
it's done. Always prefer deterministic file_exists checks (the
workhorse). The vocabulary also includes content_contains
(file substring/regex), command_exits_zero (run a command in the
shell sandbox, pass on exit 0), and http_status_is — costly hooks run
only on explicit close, not the every-cycle sweep:
const fe = (path, minBytes = 200) =>
JSON.stringify({ hooks: [{ name: 'file_exists', args: { path, minBytes } }] });
await fetch(`http://127.0.0.1:7100/api/lattices/${lattice_id}/jobs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: '<one-line job title>',
body: '<full task description with all constraints>',
why: '<why this matters>',
items: [
{
description: 'Write X.md analysing Y of the codebase. Cite file paths supporting each row.',
completion_check: fe('<absolute path to X.md>', 500),
},
// ... more items
],
}),
});The lattice's write phase auto-attempts every open item's
deterministic checks every cycle. When the lattice writes the
deliverable file via its fs-write capability, the very next cycle's
sweep sees file_exists → true and auto-closes the item. No LLM
cycles burned on bookkeeping.
Three options, choose by need:
# Poll-friendly
curl -sS http://127.0.0.1:7100/api/lattices/$LATTICE_ID | jq .
# Live SSE — every cycle's phases stream in
curl -sS -N http://127.0.0.1:7100/api/lattices/$LATTICE_ID/trace/stream
# Direct SQL on the entity (great for forensics)
sqlite3 <sqlite_path from instantiate response>
sqlite> SELECT ordinal, state, iteration_count, description FROM plan_item ORDER BY ordinal;
sqlite> SELECT cycle, body FROM memory_episodic ORDER BY id DESC LIMIT 20;
sqlite> SELECT cycle, body FROM trace WHERE kind='substrate' ORDER BY id DESC LIMIT 10;If a lattice's SQLite file exists on disk but the lattice is not in the bridge's roster (the bridge was restarted, or the lattice was explicitly stopped), resume it:
await fetch('http://127.0.0.1:7100/api/lattices', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: '<can be anything>',
identity_seed: '<not used on resume; supply a placeholder>',
tool_manifest: [...SAME_manifest_as_original_instantiate...], // important — match the original
model_backend: { kind: 'claude-code-host' },
autonomy: 'high',
resume_from_path: '<absolute path to the entity SQLite file>',
}),
});The lattice picks up at entity.cycle + 1 with full memory.
Don't change the tool_manifest's tool NAMES on resume — the
lattice's recent-actions block references prior actions by name,
and renaming them mid-life makes the prompt incoherent.
The current runtime has the following invariants baked in. Don't work around them or try to re-introduce the patterns they prevent:
- Dir-loops. The recent-actions block in
groundsurfaces the lattice's last 24 cycle-outcome memories. If you see the same action with the same input running >2 cycles in a row, the prompt instruction guides the lattice to choose differently. Don't add task-specific "don't loop" prompt instructions. - Write-without-close. The subconscious sweep auto-attempts
every open item every cycle. As soon as a deliverable file
exists on disk,
file_existspasses and the item auto-closes. You should NOT remind the lattice to manually invoke close-job-item — the sweep handles it. - Iteration-cap exhaustion via polling. The sweep uses
mode='auto'so failed polls don't consume iteration budget. The lattice's own explicitclose-job-itemcalls DO consume budget. Don't conflate them. - Source-repo modification. If the operator says "the source
is read-only", build the manifest with the source root only in
fs-read*capabilities. NEVER add anfs-writewhose outDir is inside the source repo. NEVER addshell-execwith the source as cwd unless the allowlist is restricted to read-only verbs (the default). - Wholesale copy of the source. Even if the user wants a
"port" or "local version", DO NOT copy the source repo
wholesale into the output dir. The lattice builds the port
fresh, using the source only as reference via
fs-read*. To enforce this mechanically, omitcopy/xcopy/robocopy/cp/mv/movefrom the port-shell allowlist.
- Confirm bridge is up before doing anything else.
- Read the operator's constraints carefully (local-only? read-only source? specific output dir?) and reflect them in the manifest BEFORE the lattice ever sees the task.
- Construct the job in operator-language — items the operator will recognise as deliverables, with
file_existschecks pointing at the deliverable paths. - Use
bundle_id: 'software-engineer'for codebase-analysis-or-migration work; it pre-seeds engineering heuristics into semantic memory. - Set
autonomy: 'high'for unattended runs,'medium'if the operator wants to approve destructive actions,'low'if they want every action to wait for them. - Watch via the SSE stream — don't poll faster than once-per-30-seconds for routine checks.
- Stop cleanly with
POST .../actions/stopwhen the operator is satisfied. (A lattice with no open jobs now auto-pauses —paused_no_jobs, no LLM calls — and wakes when a new job arrives; but stop it when the engagement is truly done.) - Backup the SQLite when something important happens —
sqlite3 entity.sqlite ".backup snapshot.sqlite"produces a self-contained file.
- Not a chatbot. You don't talk to it; you hand it work.
- Not a one-shot generator. It cycles continuously; the work happens across cycles, not in one prompt.
- Not stateless. The SQLite IS the entity. Treat it like a database, not a prompt.
- Not multi-tenant. One bridge per machine, single user. Public deployment was explicitly out-of-scope for v1.
- Bridge is down and you don't have authorization to boot it
- The lattice's autonomy dial is
lowand an action iswait_operator(substrate gate escalated it) - A job's iteration_count is approaching
iterationCapand the lattice can't make progress - Quota / credit concerns (this runs on the operator's coding-agent subscription when using the host-CLI backend)
- Anything that requires write access to the source repo or external systems beyond the configured tool_manifest
- The README of this repo for architecture overview
- the quickstart for a worked example
.specify/memory/constitution.mdfor the 14 principles the lattice is bound by — these are not negotiable inside the lattice's own behaviour