Bootstrap for Claude Code sessions in this repo. Content below the SHARED CONTRACT markers is kept byte-identical with AGENTS.md — CI (scripts/dev/check-shared-contract.sh) enforces parity. Edit shared rules in both files; edit the Claude preamble here only.
The installable Codex/Claude adapter bundle is canonical in the companion unitares-governance-plugin repo. This file only governs how Claude Code should behave while working directly inside the unitares server repo.
- The core must run free / self-hosted — no required metered model-API dependency. unitares is user/agent-agnostic, so a metered API (Anthropic / OpenAI / …) must never sit on the required default path: the server, CI, and residents must run with no paid key (local Ollama for Watcher,
GITHUB_TOKEN-only CI, deterministic CLI tools — ruff, the doctor, the surfacing collectors). The guarantee is for every installer, not just a funded one: someone with no model budget can always run it. Metered models are welcome as an opt-in, off-by-default backend — config-gated (e.g. abase_url/ key from env) so an operator with credits enables them and an operator without stays free. What's forbidden is forcing a paid API on everyone: a hardcoded metered endpoint, a metered CI action (anthropics/claude-code-action), or a hard SDK import with no local fallback. Add a metered path only as deferred / opt-in, and never build it inert "just in case." (scripts/dev/check-repo-scope.shRule 6 enforces this.)
Claude Code runs through a plugin-style harness. The hook lifecycle is owned by the unitares-governance-plugin repo (canonical for the adapter bundle). This repo does not vendor or wire its own hook chain. The plugin's hooks/session-start and hooks/post-edit are what fire on Claude lifecycle events — when CLAUDE.md or AGENTS.md describes hook behavior, the source of truth is the plugin.
User-level ~/.claude/hooks/ adds a third layer (auto-test, watcher-hook, watcher-chime, stop-milestone-audit, etc.) wired directly in ~/.claude/settings.json. Watcher findings that appear at session start or as a chime block originate from that user-level chain, not from the plugin's post-edit.
The per-turn check-in is the plugin's, not the user chain's. A stop-checkin.sh exists under ~/.claude/hooks/ but is no longer wired in settings.json (verified 2026-08-09; its log stops 2026-08-02). The live per-turn process_agent_update for both Claude and Codex comes from the plugin's hooks/post-stop, which submits epistemic_class="substrate_interpretation" — a substrate reading of turn shape, not an agent-authored report. Check settings.json before assuming a user-level hook is live; a file on disk under hooks/ is not evidence that it runs.
To close a Watcher finding (the agent itself lives in this repo):
python3 agents/watcher/agent.py --resolve <fingerprint> # confirmed bug, fixed
python3 agents/watcher/agent.py --dismiss <fingerprint> # false positiveReference fingerprints in the commit message — Watcher's audit trail lives in commits, not in tracked files (data/watcher/ is gitignored).
scripts/dev/session_end_stash.py is a standalone utility that captures uncommitted work into a branch-labeled git stash so intent survives session boundaries. It is not auto-fired from this repo (no hook chain wires it). Run manually, or wire from ~/.claude/hooks/ if desired:
python3 scripts/dev/session_end_stash.py.claude/CLAUDE.md is gitignored and layers on deployment-specific details (bind address, LaunchAgent paths, governance_core source symlink). Read both files; the overlay wins on conflicts.
commands/*.md— those are Codex slash commands, not Claude commands..unitares/session.json— that's the Codex continuity cache. Claude's continuity comes from the hook chain +~/.claude/projects/.../memory/MEMORY.md.
UNITARES governance MCP server. A behavioral governance framework for AI agents (EISV state vectors, coherence tracking, dialectic resolution, knowledge graph). The information-theoretic / free-energy formulation is the research target in Paper v6, not the live decision path — which is behavioral state estimation.
- Python 3.12+, asyncio
- PostgreSQL@17 + AGE 1.7.0 (Apache Graph Extension) via Homebrew
- Redis — de-facto primary session/identity store, not optional (boots in degraded local-only mode without it, but most live sessions exist only in Redis; see
docs/proposals/redis-retirement-v0.md) - Pydantic v2 for parameter validation
- MCP (Model Context Protocol) server
- Install PostgreSQL@17 with AGE extension
- Create a
governancedatabase - Install dependencies:
pip install -e . - Copy
scripts/ops/com.unitares.governance-mcp.plistto~/Library/LaunchAgents/and fill in paths/tokens (see template comments) - Start:
python src/mcp_server.py --port 8767
A single-writer surface is one where only one branch can land at a time without conflict — slot collisions, semantic merge conflicts, or strategy-divergent fixes that obsolete each other. Parallel sessions converging on these produces collisions; the 2026-04-29 migration-drift incident (#236 + #237) is the canonical example.
Before touching one of these, run gh pr list -R CIRWEL/unitares --search "in:title,body <surface-keyword>" --state open and the same against CIRWEL/unitares-governance-plugin for cross-repo surfaces. If an in-flight PR exists, comment there or branch from its head — do not start a parallel attempt.
Surfaces:
- Migration slots and migration-drift fixes —
db/postgres/migrations/. Now CI-gated byscripts/dev/unitares_doctor.py; the doctor fails on slot/name drift, but session-level coordination still avoids wasted parallel work. - Identity / onboarding — docs AND implementing code are one coupled surface — docs (
docs/ontology/identity.md,commands/governance-start.md,skills/governance-lifecycle/SKILL.md, theAGENTS.md/CLAUDE.mdshared contract including theStrict Identity, Simple Contractblock below, theforce_new=true/parent_agent_idposture) AND code (src/mcp_handlers/identity/,src/mcp_handlers/middleware/identity_step.py,src/mcp_handlers/support/agent_auth.py,src/mcp_handlers/schemas/identity.py). Treat as a single writer-locked region, not as separate doc/code workstreams. These also flow across two repos (unitares + gov-plugin); check both. docs/ontology/plan.md— chronological state ledger; two sessions appending rows in the same window collide trivially. If a session is already editing it, branch from its head rather than starting parallel.- Active proposal/RFC docs in hot phase — the Plexus / lease-plane / BEAM thread (
docs/proposals/plexus-scope.md,surface-lease-plane-v0.md,surface-lease-plane-phase-a-plan.md,beam-footprint-roadmap-v0.md,beam-coordination-kernel.md). Restructure-during-flight is normal here; same rule as plan.md: branch from another session's head if one is in flight. - Large test-layout consolidation —
tests/directory. If you're about to delete more than ~200 lines of tests, surface intent in a draft PR or issue first; a stale −3496 diff (feat/agentskills-compat) was lost to drift this way.
This section protects against wasted parallel work across agents, not just mistakes within one session. The deeper fix is upstream of any single agent: do not run multiple agents on the same single-writer surface in the same window.
- Run
./scripts/dev/test-cache.shbefore the first commit in a local change sequence (tree-hash cache — skips if tests already passed against this exact test input tree; use--freshto force a re-run) - Use
./scripts/dev/test-cache.sh --stagedwhen validating a staged subset; it hashes the staged commit candidate and refuses to run if unstaged/untracked files would affect pytest - After a branch is pushed and GitHub CI is running the full gate, do not restart local full
test-cacheruns for every fixup; run focused local tests for the touched behavior, push, and let CI be the final full gate - If you edit
AGENTS.mdorCLAUDE.md, also run./scripts/dev/check-shared-contract.shto confirm the shared block stayed in sync - Fix any test failures your changes introduce — do not commit broken tests
- If you change a function's behavior or signature, update its tests in the same commit
- If you do a mechanical refactor (renames, import changes), update affected test mocks before committing
- The pre-push hook will block pushes with test failures
- governance_core lives in this repo at top-level
governance_core/(pure Python). Code insrc/imports it asfrom governance_core import X. - LazyMCPServer: All handler modules import
lazy_mcp_server as mcp_serverfromshared.py(single definition, no per-file copies). Tests patch{MODULE}.mcp_servernotget_mcp_server. - Pydantic validation: Parameter validation uses Pydantic schemas in
src/mcp_handlers/schemas/. Legacyvalidate_and_coerce_paramsis removed. - Handler modules: Each in
src/mcp_handlers/, decorated with@mcp_tool.
- PostgreSQL@17 on port 5432 with AGE graph extension
- Requires
brew services start postgresql@17 - Check connectivity:
pg_isready -h localhost -p 5432 - Do NOT create additional PostgreSQL instances, databases, or migration layers
- Do not force-push in a way that can lose work. The rule is about work loss, not about rewriting history as such: amending your own un-merged commit — a message fix, a squash of your own commits — is fine. Push it with both
--force-with-lease --force-if-includes. Both are required, and--force-with-leasealone is not the safeguard it is widely assumed to be: it only checks that the remote ref still matches your remote-tracking ref, so anygit fetchsince your last integration silently refreshes the lease and the next force-push destroys a colleague's commit without warning. Agent sessions fetch constantly, so this is the normal case, not a corner case.--force-if-includescloses it by additionally requiring that what you are about to overwrite is already reachable from your local history. Never force-push a shared or already-merged branch, never force-push over a commit you did not author, and never use bare--force. - Do not run destructive git commands without explicit user approval
- Do not run DROP/TRUNCATE/DELETE on the governance database without explicit user approval
- Keep harness session-attribution trailers out of commit messages.
repo-scope.ymlgreps the wholeorigin/master..HEADrange for the session-link trailer, so a later commit cannot clear a bad one — the only repair is an amend. Agent harnesses append this trailer by default; strip it before committing. The exact pattern is defined inscripts/dev/check-repo-scope.shand is deliberately not reproduced here, because writing it out trips the guard on this file.
Codex and Claude share one delivery contract so concurrent sessions stay predictable. Full reference: docs/operations/github-workflow-conventions.md.
- Branch naming — one pattern, author-prefixed:
<author>/<topic>-<short-id>where<author>identifies who is making the change — an agent (claude,codex) or a contributor handle — for parallel attribution. Bothship.sh's<author>/auto/<timestamp>-<slug>and the web harness'sclaude/<topic>-<id>satisfy this shape. Never push tomain/master. - Delivery — draft PR for everything: every session lands its work as a draft PR, regardless of agent and regardless of whether the change is runtime code or docs/tests. A human maintainer is the merge gate. Do NOT direct-push to a shared branch and do NOT enable auto-merge by default.
ship.shenforces this: its defaultautoroute opens a draft PR for every change (--directopts out for docs/tests-only pushes;--auto-mergeonly when a maintainer explicitly asks). - Delivery requests authorize delivery: when a maintainer asks to ship, finish, deliver, open a PR, or otherwise complete a delivery workflow, the working agent may assume branch -> commit -> push -> draft PR is in scope and should not ask for a second confirmation just to push or open the draft PR.
- Mark-ready / merge is a deliberate action: a draft PR means "visible, not claiming merged." Only mark ready or merge after CI is green and you've confirmed no collision with an in-flight branch (see the single-writer-surface rules above).
The MCP SDK runs handlers inside an anyio task group. asyncpg and Redis run on Python's asyncio. When a handler awaits DB/Redis work, the two scheduler models can interact in ways that hold connections across unrelated awaits and amplify latency by orders of magnitude. Measured 2026-05-04 on the governance-MCP request path: KG calls that complete in 21–71ms standalone run at ~4,464ms in-handler — a ~60× amplification, with the floor sub-100ms and the rest in scheduling / pool-acquisition / event-loop contention. The Sentinel-loop call site (agents/sentinel/agent.py:416-459) is mitigated to ">400 cycles, zero failures" via PR #290, but that fix is one workaround at one site, not closure of the bug class.
These are workarounds, not architecture. The patterns below accreted from incidents — three over the last year, with new variants emerging on different surfaces (current example: the load_metadata_async N-await loop on observe handlers, see PR #348 follow-up). The bug class is structural to anyio + asyncio + asyncpg / Redis on a shared event loop and does not exist on substrates with per-process scheduling and protocol-level connection checkout (e.g., BEAM / db_connection).
As of PR #218 (deployed 2026-04-27), get_db() returns an ExecutorPool-wrapped backend (src/db/executor_pool.py). asyncpg operations run on a dedicated background thread with its own event loop, so the anyio task group never sees an asyncpg await. New handlers can use async with db.acquire() as conn: await conn.fetchval(...) directly — no wrapper needed for asyncpg DB work. Redis async clients are not yet wrapped by ExecutorPool. Existing Redis asyncio.wait_for timeouts in identity_step.py, persistence.py, and session.py remain as a precaution; do not add new ones for asyncpg but leave Redis guards in place.
The three patterns below were the pre-ExecutorPool workarounds. They are retired for new asyncpg handlers but remain in the codebase as historical context and where they serve purposes beyond anyio isolation (Redis guards, sync blocking I/O, performance caches):
- Read cached data populated by a background task (e.g.,
health_checkreadsdeep_health_probe_task's snapshot; sticky identity reads a cache pre-warmed bytransport_binding_cache_warmup). run_in_executorwith a sync client — seeverify_agent_ownershipdispatch atsrc/agent_loop_detection.py:403(synchronous DB-touching function pushed to an executor thread so the anyio task group stays unblocked). The same pattern is used externally bycall_pi_toolin theunitares_pi_pluginpackage.asyncio.wait_forwith a tight timeout — degrade to a fallback on deadlock instead of hanging the pipeline. Seedeep_health_probe_taskatsrc/background_tasks.py:545and_load_binding_from_redisatsrc/mcp_handlers/middleware/identity_step.py(500ms budget, returnsNoneon timeout).
- Knowledge graph AGE tests require a live AGE connection (errors, not failures, when unavailable)
Strict identity is a write gate. Reads may work without a bound caller; writes must be accountable. Agents should not need the full identity ontology for the normal path.
Operational rules:
- Start each driver with
start_session(force_new=true)(onboardis the canonical tool underneath). Save the returneduuidandclient_session_id. - For later check-ins and writes in the same running process, pass
client_session_id. Adapters should do this automatically. If your adapter does not thread it, you fall back to the weak transport-fingerprint pin and can fragment under co-residency — checksession_source/tierin the onboard response to confirm you bound as expected. - To continue prior work in a fresh process, mint fresh and declare the cause:
start_session(force_new=true, parent_agent_id=<prior_uuid>, spawn_reason="new_session"). Use this only for a real handoff from a finished predecessor. - Short dispatched subagents usually should not onboard. If one needs its own
identity, use
spawn_reason="subagent", setparent_agent_id=<driver_uuid>, and land at least one realsync_state()before exit. - Persistent/substrate agents use their dedicated substrate identity pattern. Ordinary sessions should not copy that pattern.
Do not do these in normal agent code:
- Bare
onboard()oridentity()as a way to guess identity. - Passing
continuity_tokenon every call. - Treating a display name as identity.
- Declaring
parent_agent_idjust because another session shares the workspace. - Writing KG notes before searching for an existing entry.
Minimal glossary:
uuid: the server record for this process identity.client_session_id: the proof string for this running process. Use it on writes; do not treat it as cross-process selfhood.parent_agent_id: a causal pointer to the process whose work this process is inheriting.lineage: "this process inherited work from that one," not "this process is that one."continuity_token: advanced same-live-process rebind proof. Not part of the normal workflow.
Friendly workflow aliases: start_session -> onboard, sync_state ->
process_agent_update, check_working_state -> get_governance_metrics,
search_shared_memory -> knowledge(action="search"), record_result ->
outcome_event, and request_review -> dialectic(action="request").
Shared-memory (KG) write discipline: search before writing. If a related entry
exists, prefer a linked correction or supersede over a fresh note. Store when
a future agent would search for this and not already find it: a correction to a
prior conclusion, a non-obvious failure mode plus its fingerprint, or a closed
mystery. Operational runbooks and step lists belong in docs/, not KG notes.
Tag recurring point-in-time snapshots ephemeral. This is a claim about the
content's shelf life, never about the writer's. A session that ends in an hour
can still record something permanent, and must not tag it ephemeral on the
grounds that the session was short-lived. The test is what the entry is: a
reading taken at a moment (weekly triage run, scheduled audit, per-cycle status
note) gets the tag; a claim meant to stay true (a diagnosis, a closed mystery, a
correction to a prior conclusion) never does. Vigil's groundskeeper tags its own
notes this way.
Without the tag, snapshots land as ordinary open entries and never close: a
snapshot has no resolution condition, only a timestamp, so every later sweep
re-reads it as unfinished work. With it, KnowledgeGraphLifecycle archives
after seven days — archived, still retrievable via include_archived=true,
never deleted (src/knowledge_graph_lifecycle.py:139).
Two traps. EPHEMERAL_TAGS also contains test and demo, so a durable
finding about the test suite must not carry the test tag. And permanence
wins on tie: a learning / pattern / root_cause_analysis / migration
type, or a permanent / foundational / architecture / decision tag,
overrides an ephemeral tag rather than losing to it — get_lifecycle_policy
checks permanence first (src/knowledge_graph_lifecycle.py:107).