Skip to content

Repository files navigation

Coniunctio Intelligentiarum

A harness for AI coding agents that makes three things true: the safety rules can't be skipped, what was learned isn't forgotten, and nothing is called "done" without evidence.

What this is, in plain terms

If you work with an AI coding agent for more than an hour, you meet three problems.

First, the agent drifts. You tell it "never commit secrets, always run the security check" and it complies — for a while. Deep into a long session the rule quietly stops firing. Nothing announces this. Every other signal still looks healthy.

Second, the agent forgets. The bug you solved together last Tuesday, the approach that worked, the decision you already made — a new session starts blank, and you pay for the same ground twice.

Third, the agent grades its own homework. Ask it to write a tool and also the tests for that tool, and it will happily report success measured against its own imagination. We watched a tool score 92% on the test corpus it wrote for itself, and 8.8% on reality.

Coniunctio's answer to all three is the same idea: stop asking the AI to remember rules, and build the rules into machinery the AI cannot skip. The checks run outside the model — as git hooks and session hooks — so they fire whether the agent remembers them or not, whether the agent even agrees or not. A risky change with no evidence attached is refused at the commit boundary, by git, not by a prompt. What was learned is written to files and re-surfaced automatically at the start of every session. And "the tests pass" only counts when the tests were run against the world, not against the author's assumptions.

It was extracted from a working private system, where it gates real commits every day — including, more than once, its own author's. It is early, and honest about what it does and doesn't do.

Since 0.4.0 this repository ships that system whole, not just its gates: the message bus several AI seats coordinate over, the knowledge commons they share, the loop layer that makes "done" a command instead of a claim, and the prompt guard that stops a secret before it leaves your machine. 0.5.0 adds the layer that watches the watchers — the evaluator tripwire, which tells you at session start whether the gates you are relying on are still the gates you wrote. Each section below starts in plain words and gets technical after.

A control that must be invited is not a boundary.

Quickstart

# 1. use this repo as a template for your project, then in your project:
git config core.hooksPath scripts/git-hooks       # activates the gates
# 2. opt in: .claude/harness-gates.json ships with {"enabled": true}
# 3. (Claude Code users) copy the hooks from .claude/settings.example.json
#    into your .claude/settings.json and restart the session — recall is online

Nothing activates in a project that hasn't opted in. Prove the machinery any time:

python3 scripts/harness-gates/commit_gate_test.py   # builds real repos, runs real commits
python3 scripts/harness-gates/push_gate_test.py     # real pushes, refused where they should be

Your first risk-triggering commit will be blocked. That is the gate working. It wants an evidence record committed alongside the change — copy docs/status/verification/example.json, fill it in honestly, stage it. If the gate is wrong in your case, [gate-ok: your reason] in the commit message is the deliberate, logged way past it.

How it works

Three layers:

  1. Unskippable mechanisms — plain git hooks (scripts/git-hooks/) and thin session hooks (scripts/harness-gates/). The enforcement point is a git commit-msg hook, so it holds under any agent, or none.
  2. Per-project content — your risk patterns, your evidence records, your strategy cards. The mechanism is shared; the values are yours.
  3. Template machinery — what keeps N projects' copies from drifting apart.

The pieces:

  • The verification gate. A commit touching a risk pattern (auth, secrets, hooks, schema, the gate itself…) must carry a verification record that binds to the diff: names every risk-triggering path, cites non-empty evidence, sits inside a freshness window, and is read from the git index — so a record can't be staged empty and repaired on disk after. Merges are gated on the delta beyond the mechanical merge, so conflict resolutions are checked but already-gated branch work isn't re-litigated.
  • The push boundary. Git never runs commit-msg for cherry-pick, revert, rebase, or git am — git facts, not fixable bugs. So pre-push re-scans every outgoing commit with the same rules (push_gate.py), and pre-applypatch covers git am at apply time. History made by any silent path is re-checked before it leaves the machine.
  • Memory & recall. A tiny always-on index is injected at session start; full detail is pulled on demand. "We've solved this before" is in front of the agent with nothing to remember.
  • The strategy repertoire. The agent banks the method it used, not only the fix, and is shown the best-fit strategy at the start of the next problem — a human skill, rebuilt in a form an AI can run.
  • The watchdog. Every gate decision is logged with a counted verdict vocabulary; the session banner reports blocks, overrides, errors, fail-opens, and gate-disabled commits. A gate that silently stopped working is the failure mode this whole project exists to prevent, so the gate's own health is surfaced mechanically — and a consistency check (scripts/check-harness.py) proves every verdict a gate can write is one the watchdog can count, so the two can never drift apart silently again.
  • The message bus (scripts/bus/) — several AI seats, one file-backed mailbox. Own section below.
  • The knowledge commons (scripts/commons/) — shared recall with a hard write fence. Own section below.
  • The loop layer (scripts/qas/, docs/status/away-queue.example.json, scripts/trace/) — a mechanical done-verifier, a machine-drainable work queue, and structured traces. Own section below.
  • The prompt guard (scripts/prompt-guard/) — blocks a secret-bearing prompt or a secret-file read before it leaves the machine. Own section below.

Full design reasoning, with the measurements that motivated it: docs/design/enforcement-not-prose, memory-and-recall, strategy-repertoire, why-tests-pass-but-tools-are-weak.

Several AIs, one mailbox — the message bus

If you run more than one AI — say a cloud orchestrator and a local model — they need a way to talk that isn't you copy-pasting between windows. The bus is that way: a mailbox on disk. Every message is a small markdown file you can read with cat and audit with git log; nothing is a database, nothing needs a network.

The parts that took real incidents to learn:

  • Identity is a launch argument, not a message field. A seat's identity comes from how its server process was started (--seat), so no seat can impersonate another by saying it is someone else — and a newline smuggled into a subject line can't forge the from: header either (both were live-proven attacks; both are regression-tested).
  • Message bodies are data, never instructions. Every body is wrapped in an untrusted-content fence before any seat reads it. We probed this for real: an injection sent to a 30B local model got partial compliance despite the fence — the model printed the attacker's token, then refused the rest. The lesson is architectural, not cosmetic: prompting is not a boundary, so the local-model seat has no tools. A seat that cannot act cannot be talked into acting.
  • Turn control that never destroys speech. Passing the turn while you still owe replies gets your message delivered but downgraded, with the reason recorded in the file — the bus never drops a message body to enforce a rule.
  • Secrets are scrubbed at write time. Local models run outside any harness and never agreed to a no-secrets policy, so the bus enforces it mechanically: every subject and body passes through the scrubber (learning-system/scrub.py) before touching disk, and the redaction count is recorded in the message header so nothing is silently redacted.
  • Flood ceilings. Size and rate limits refuse (never truncate) an oversized or over-frequent sender.

Prove the whole loop with nothing but Python — two seats, one round trip:

python3 - <<'EOF'
import json, sys
sys.path.insert(0, "scripts/bus")
import core
core.BUS_ROOT.mkdir(parents=True, exist_ok=True)
core.SEATS_FILE.write_text(json.dumps({"seats": [
    {"id": "orchestrator", "name": "Orchestrator", "role": "orchestrator", "transport": "mcp"},
    {"id": "helper", "name": "Helper", "role": "reviewer", "transport": "mcp"},
]}, indent=1))
core.ensure_layout()
core.send_message("orchestrator", "helper", "ping", "What is 2+2?")
msg = core.read_messages("helper", unread_only=True)[0]
print(core.render_for_display(msg))            # arrives inside the untrusted-content fence
core.ack_messages("helper", [msg.id])
core.send_message("helper", "orchestrator", "pong", "4")
print(core.read_messages("orchestrator", unread_only=True)[0].body)
EOF

core.py is dependency-free; the tests (python3 scripts/bus/test_core.py, 50 of them) run the same way. Serving a seat over MCP (scripts/bus/server.py) needs the mcp Python package; driving a real local model as a seat (scripts/bus/local_model_seat.py) needs any OpenAI-compatible endpoint (LM Studio and Ollama both work). Details, layout, and the dated injection findings: scripts/bus/README.md.

What one AI learns, every AI can find — the knowledge commons

A lesson learned in one session is usually lost to the next, and a lesson learned by one model never reaches the others. The commons is a shared shelf of markdown notes — outside any single repo — that every seat can query, with a write path deliberately harder than the read path.

  • Write capability is a permission scope, not a promise. A seat can write only if it was launched with --write quarantine; there is no tool parameter that grants it, so a model cannot talk itself (or another seat) into write access.
  • Everything a seat writes lands in quarantine, behind the same nonce-marked untrusted-content fence the bus uses. Promotion to the trusted shelf is a human moving a file — there is deliberately no promote() function to exploit, and the test suite asserts its absence. The commons is cross-project, so a drivable promotion path would turn one injection into fleet-wide corruption.
  • Knowledge carries a freshness date. Entries declare when they were verified and for how long that holds; a stale entry is returned marked stale, never silently hidden and never silently trusted.

python3 scripts/commons/test_core.py (39 tests) proves all of this against a throwaway vault — no setup needed. Using it for real needs a vault directory (set LEARNING_DATA_DIR, plus LEARNING_COMMONS_DIR for the shared shelf) and, for semantic search, a local embeddings endpoint (learning-system/rag.py speaks to any OpenAI-compatible /v1/embeddings; keyword-free recall degrades gracefully without one). Serving it over MCP (scripts/commons/server.py) needs the mcp package. Details: scripts/commons/README.md.

"Done" is a command, not a claim — the loop layer

Ask an agent "is it done?" and you get prose. Prose can be argued with. This layer makes "done" something you run:

  • The mechanical done-verifier (scripts/qas/verify.py) walks nine definition-of-done items and answers each one mechanically where a machine can — suites actually run and actually exit 0, risk-triggering changes carry a verification record that names the changed files, no secrets in the added lines, no ⛔ STOP marker built past — and answers honestly where only a human can: those come back NEEDS-OWNER, never faked green. One run, one verdict: exit 0 pass, exit 2 needs-owner, exit 1 fail. Configure your project's suites in .claude/qas-verify.json (ships pre-filled with every suite in this repo; replace with your own).
  • The away-queue (docs/status/away-queue.example.json) is a work queue a machine can drain without misreading prose: every item declares actionable (may an autonomous run take it?), blocked_on (required when it may not), and acceptance (how a run knows it's done). The schema is enforced by scripts/check-harness.py and mutation-tested — an item that says "do not act" only in prose is exactly what this retires.
  • Structured traces (scripts/trace/trace.py) — one JSONL line per AI event, OpenTelemetry GenAI field names, counts and verdicts only. It refuses content at the type level: any key named like prompt/message/text/body is dropped before writing, so the trace stream cannot leak what was said. A broken tracer never breaks the traced tool (proven by its tests).
python3 scripts/qas/verify.py            # the nine checks, one verdict (needs a git repo)
python3 scripts/trace/trace_test.py      # content-refusal + fail-open, proven
python3 scripts/harness-gates/away_queue_test.py   # the queue schema, mutation-tested

Who checks the checkers — the evaluator tripwire

Every gate above is a file in your repo, which means an agent working in your repo can edit it. Nothing in a session can tell you whether the checkers it is running are still the checkers you wrote — and a weakened gate reports exactly like a healthy one. That's the gap this closes.

You declare which files are the judges (the gates, their suites, the secret scanner — whatever must not drift). Then two independent mechanisms watch them:

  • Attention at the door. Those paths get ask permission rules, so editing a judge through the editing tools prompts you instead of happening quietly. This is a speed bump, not a wall: a shell command writes the same file without asking. That is precisely why the second mechanism exists, and why the harness never treats the ask list as a boundary — its own checker says so in as many words.
  • Detection against a reference the session can't move. At session start the harness re-reads the declared set, hashes it, and compares against origin/main as freshly fetched — not the local tracking ref, which a session can rewrite. Offline, it falls back to a manifest written at push time, and prints the manifest's age rather than treating a stale reference as a current one.

The verdict is one line, and its wording is load-bearing. A match says so. A divergence names the files, and never truncates a removed one. Deleting the declaration entirely does not buy silence — that reads as deletion, not as "not configured". And when the harness cannot positively vouch for the reference it fetched, it prints a plain comparison instead of a clean bill of health: the absence of a contradiction is not evidence of a match. Getting that distinction wrong is what an integrity reporter is for, so the suite (evaluator_tripwire_test.py, 87 cases) is built as mutation cases against real bare remotes — each one weakening the tripwire on purpose and checking a case goes red.

python3 scripts/harness-gates/evaluator_tripwire_test.py   # 87/87, ~45s (builds real git remotes)

Nothing is watched until you declare a set, and the harness says "not configured" rather than implying protection you haven't asked for. Adopting it is two edits: declare an evaluator_set path list in docs/harness/rule-owners.json, and give each path Edit/Write ask-rules in your settings — check-harness.py fails when those two disagree, so the declaration and the permissions can't drift apart. The push-time manifest is written by scripts/harness-gates/write_evaluator_manifest.py, which the shipped pre-push hook already calls (warning, never blocking, if it can't).

Stop the secret before it leaves — the prompt guard

Pasting a credential into an AI chat is not recoverable — once sent, rotation is the only fix. The guard is a pre-send hook: it inspects what you typed locally, before transmission, and blocks the send if it contains something secret-shaped; a companion mode blocks tool calls that would read a secret file (.env, key files, credential stores), because a tool result enters the conversation exactly like typed text.

Two honest numbers from building it: the guard's hand-written tests scored 92% on the corpus its author wrote — and 8.8% against real credentials. That gap is why the strong layer matches known values read from your own secret files rather than guessing shapes, and why the mutation suite (scripts/prompt-guard/mutation_test.py) breaks the guard on purpose and checks the tests notice. One known hole is documented flat in the file header: messages typed mid-turn bypass the hook entirely — the guard covers normal turns only, and says so.

python3 scripts/prompt-guard/prompt_guard.py selftest
python3 scripts/prompt-guard/mutation_test.py      # 7/7 deliberate breakages noticed

Wiring it into Claude Code is two hook entries — see the header of scripts/prompt-guard/prompt_guard.py.

Honest limits

  • The gate can't confirm the review a record describes actually ran. A binding record is strong process evidence, not proof of review.
  • git commit --no-verify skips any commit-msg hook, and git push --no-verify skips the push re-scan — explicit, typed, visible acts. The gates stop accidental skips; nothing here stops an actor determined to evade its own gates, and no design that claims to should be believed.
  • A commit that replaces the hook shim itself lands ungated (git runs hooks from the worktree) — asserted in the test suite as a known limit, caught one boundary out at pre-push.
  • It fails open, loudly: a bug in the gate warns, logs a counted verdict (ERROR when it blocked, FAIL-OPEN when an unexamined commit went through), and lets your commit through rather than locking you out. "fail_closed": true opts into block-on-error. A process gate is a seatbelt, not a containment wall.
  • Recall and self-healing use Claude Code's hook system today; enforcement is agent-independent, the session wiring is not yet.
  • The bus's untrusted-content fence reduces injection, it does not prevent it — measured, dated, and written into scripts/bus/README.md. The real containment is that the local-model seat has no tools. Keep that ceiling.
  • The prompt guard does not see messages typed mid-turn (a platform gap, verified live and documented in the file header). It is a seatbelt for normal turns, nothing more.
  • verify.py is run by the same seat that did the work — it is the mechanical floor under verification, not independence. What it guarantees is that the listed suites truly ran and truly exited 0; judgment items come back NEEDS-OWNER instead of green.
  • The evaluator tripwire is a reporter, not a boundary — and these are the limits we know of. Its reference is only as external as your remote: the pin records the remote's URL string, so git config that changes where that URL actually resolves is not covered by it. The offline manifest lives outside the repo and is writable by the same session it describes, so it raises an attacker's cost by roughly one command rather than creating a wall — which is why the wording never claims more than "as of this manifest, this age". The manifest's recorded commit id is copied into the verdict but not independently resolved. Hashing is one git show per declared file, so a very large declared set can outrun a session-start hook's time budget. And if the declaration, the config, the manifest and the remote are destroyed in one move, no reference remains: that state is genuinely indistinguishable from a project that never adopted the mechanism, and the harness says the honest thing rather than guessing. Each of these is tracked; none of them is hidden behind a green tick.
  • The MCP servers (scripts/bus/server.py, scripts/commons/server.py) need the mcp Python package; everything else here is stdlib-only. Semantic recall needs a local embeddings endpoint.
  • Interfaces will change.

Design principles

  • Structural, not procedural. If a control can be skipped or forgotten, it isn't one.
  • Human always on top. The agent proposes; the human decides what matters — merges, money, anything irreversible.
  • Honest about limits. A tool that looks like it works but doesn't is worse than one that admits its edge. This project writes its own failures down.
  • Adopt, don't reinvent. Prior art is raw material, never a rival.

Why this repository is public

Coniunctio was built by one person, working with an AI, in private. Each release is a generated artifact of that private canonical system — scrubbed, checked, and published as a fresh snapshot, never hand-edited in place. It's public for two reasons:

  1. To give the work away. Agents that quietly stop following rules, tools that pass their own tests and fail in the world — these are everyone's problems right now. If these mechanisms save you the months they cost, take them.
  2. To learn from people who know more. This was built without a team. If you've solved a piece of it better, an issue or a pull request showing the stronger way is exactly what this repository is open for.

The name is Latin for the union of intelligences — a human mind and an AI mind, each covering where the other is weak, with the human always on top. That union gets stronger with more minds in it; that's the invitation.

Contributing

Friendliest first step: an issue describing what you'd change — or a pull request if you already have it. Everything comes in by fork → PR; the maintainer reviews and merges. Contributors sign a lightweight CLA so changes can be safely maintained. See CONTRIBUTING.md and SECURITY.md.

License

Apache-2.0 — chosen for its explicit patent grant, the norm in this field.

Acknowledgements

Built with Claude Code. Coniunctio stands on prior art in the agent-harness space — Goose, Aider, Letta, and LangGraph among others — whose mechanisms it studied, adopted, and credits.

About

The union of intelligences — a harness that bridges a human mind and an AI mind: unskippable gates, structural memory & recall, and a growing repertoire of how-we-solve. Human always on top.

Topics

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages