P0.7.2 — Step 1 Audit

Inventory of the worktree against the task spec for P0.7.2 ζ Thought Record CRUD (ζ Decision Trail task group, second ζ task, Wave D parallel dispatch). Scope: what already exists that the new repository + MCP tools must integrate with, what the P0.7.1 primitives lock, and what is still absent.

Baseline: worktree E:/AMS/.worktrees/claude/p0-7-2-thought-crud/ at commit 6c26bb58 (P0.7.1 merged as PR #124, on top of P0.6.1 cf1250ca + P0.3.1 b8a036a6 + P0.2.3 92df616d).


§1. Pre-clean state

$ git status
On branch feature/p0-7-2-thought-crud
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean

$ git diff --stat
(empty)

$ git log -1 --format=%s
feat(p0-7-1): ζ hash-chained record schema — SHA-256 canonical JSON + ZERO_HASH genesis (#124)

No cross-worktree leak. The Wave C P0.7.1 bug (cross-worktree leak of src/server.ts edit) is not present here. Starting state is pristine main.


§2. Surface being added

Targets this task creates:

  • src/domains/trail/repository.ts — new module. Holds createThoughtRecord(db, input), getThoughtRecord(db, id), listThoughtRecords(db, filters), registerThoughtTools(ctx, db). Consumes P0.7.1 primitives (computeHash, canonicalize, ZERO_HASH, ThoughtType, ThoughtRecord, ThoughtRecordSchema).
  • src/__tests__/domains/trail/repository.test.ts — new nested test file. First use of src/__tests__/domains/<concept>/ layout (Wave A-C used flat src/__tests__/<concept>-<suffix>.test.ts). Jest testMatch: ['**/__tests__/**/*.test.ts', ...] supports nested discovery; roots: ['<rootDir>/src'] covers it.
  • src/db/migrations/004_thought_records.sql — number 004 pre-assigned by Sigma (P0.3.2 owns 002, P0.6.2 owns 003). Introduces thought_records table + two indexes.
  • src/db/schema.sql — append thought_records ownership block (comment-only asset, non-executable).
  • src/server.ts — small edit in bootstrap(): register ζ tools after server_ping. Expected 3-way rebase collision with P0.2.4 / P0.6.2.

A worktree scan confirms absence of authoring targets:

$ ls src/domains/trail/
schema.ts                          # P0.7.1 only — repository.ts absent
$ ls src/db/migrations/
001_init.sql                       # 002/003/004 all absent
$ ls src/__tests__/domains/        # directory absent

§3. Adjacent code — consumers and dependencies

3a. src/domains/trail/schema.ts (P0.7.1 — 188 LOC, commit 6c26bb58)

Exports exactly 7 identifiers this task will consume:

Export Kind Consumed by
THOUGHT_TYPES readonly [...] tuple of 4 literals createThoughtRecord input validation (via Zod z.enum)
ThoughtType type alias union createThoughtRecord input type + tool Zod schema
ZERO_HASH string (64 zeros) createThoughtRecord genesis fallback when per-task chain empty
ThoughtRecordSchema z.ZodObject Not directly consumed — repository enforces stricter shape via explicit types
ThoughtRecord z.infer<...> type Return type for createThoughtRecord / getThoughtRecord
canonicalize pure fn Not directly consumed — internal to computeHash
computeHash pure fn over 6-field subset Invoked once in createThoughtRecord to produce each record’s hash

Load-bearing field constraints from P0.7.1 Zod schema (schema.ts lines 80-89):

export const ThoughtRecordSchema = z.object({
  id: z.string().min(1),           // REQUIRED non-empty
  type: z.enum(THOUGHT_TYPES),     // REQUIRED, one of 4 values
  task_id: z.string().min(1),      // REQUIRED non-empty (NOT nullable)
  agent_id: z.string().min(1),     // REQUIRED non-empty (NOT nullable)
  content: z.string(),             // REQUIRED, empty-string allowed
  timestamp: z.string().min(1),    // REQUIRED non-empty
  prev_hash: z.string().length(64), // REQUIRED exactly 64 chars
  hash: z.string().length(64),     // REQUIRED exactly 64 chars
});

Deviation from dispatch prompt §7: The prompt baseline says task_id TEXT (nullable — global thoughts have no task) and agent_id excluded from hash per P0.7.1. However, P0.7.1’s Zod schema has both task_id and agent_id as .min(1) required non-empty strings. The prompt itself §5 says “reuse P0.7.1 primitives; do NOT reimplement”. Decision: honour P0.7.1’s Zod schema as source of truth. Both task_id and agent_id are required non-empty. There is no “global thought” path in Phase 0 — every thought record belongs to a task. If a caller lacks a task_id, they must mint one (or use a sentinel string agreed upstream). This matches P0.7.1 contract §3 “task_id — scopes the chain to a task. Prevents cross-task forgery.”

The agent_id default “claude” is acceptable since all Phase 0 execution is Claude (T4). Callers may override.

3b. src/__tests__/trail-schema.test.ts (P0.7.1 — 353 LOC)

  • Fixture value: VALID_SUBSET with {id:'r1', type:'plan', task_id:'t1', content:'hello', timestamp:'2026-04-17T00:00:00Z', prev_hash:ZERO_HASH} → pinned hash 6a2f9597f563d5515cfa69891a51806d0f93bfbe222997d3ba37c365ceee3f1a.
  • Verified via node -e: the canonical-JSON + SHA-256 of that input reproduces the pinned hash exactly. P0.7.2 tests will re-assert this hash against computeHash(VALID_SUBSET) — serves as a cross-test determinism anchor.
  • Insertion-order-agnostic tests, Unicode, 1 KB content, type-sensitivity all pinned.
  • Test style: flat describe blocks, no env mutation, no jest.isolateModulesAsync.

3c. src/db/index.ts (P0.2.2 — 320 LOC)

initDb(path) + getDb() + closeDb(). Migration runner applies NNN_*.sql files in numeric order, bumps PRAGMA user_version per migration, wraps each in a transaction.

Consumption pattern for P0.7.2:

  • Tests: construct in-memory DBs via new Database(':memory:') directly, then apply migration 004 SQL manually (pattern: db.exec(migrationSql)). Matches P0.3.1 / P0.6.1 test style when DB-backed (here: first such test in the suite).
  • Production: startup.ts calls initDb(config.COLIBRI_DB_PATH) in Phase 2, which discovers and applies 004_thought_records.sql automatically.
  • Repository functions take a Database.Database parameter (dependency injection) so tests don’t need to touch getDb().

Migration file naming: NNN_<slug>.sql where NNN is a positive integer prefix, followed by underscore, followed by a human-readable slug. Collision detection is built-in (throws on duplicate prefix). 004 is pre-assigned to this task. Filename: 004_thought_records.sql.

3d. src/server.ts (P0.2.1 — 568 LOC) + src/startup.ts (P0.2.3 — 424 LOC)

  • registerColibriTool(ctx, name, config, handler) — 5-stage α middleware wrapper. Input name is validated via TOOL_NAME_RE = /^[a-z_][a-z0-9_]*$/ (snake_case, leading letter/underscore). thought_record and thought_record_list both match.
  • Handler signature: (args: z.infer<z.ZodObject<I>>) => Promise<unknown> | unknown. Return value becomes envelope.data in the {ok: true, data: ...} wire envelope.
  • bootstrap() registers server_ping then calls start(). This task must extend bootstrap() to register ζ tools after server_ping. Signature change needed: bootstrap() must thread a DB handle through, or register ζ tools via a post-Phase-2 hook. Current bootstrap() runs entirely in Phase 1 (no DB) — but the ζ tools need a DB handle at call time, not at registration time.

Resolution: Register ζ tool handlers that lazy-resolve the DB at each invocation via getDb(). This avoids changing the bootstrap() signature and matches the spirit of the Phase 2 handoff (DB is initialized by startup.ts Phase 2 before any tool call can fire; registerColibriTool executes only at registerTool time, not at handler-invocation time, so registering at bootstrap() is safe). The registerThoughtTools(ctx) function doesn’t even need a db argument — handlers call getDb() internally.

For test injection: the repository core functions (createThoughtRecord, getThoughtRecord, listThoughtRecords) take an explicit db argument. Only the registerThoughtTools wrapper closure reaches for getDb(). Tests exercise the core directly (with in-memory DB) AND validate the MCP tool envelope via InMemoryTransport + a DB-setter seam.

Diff to server.ts bootstrap() — minimal, 2 lines imported + 1 call site:

// +import { registerThoughtTools } from './domains/trail/repository.js';
// inside bootstrap(), after registerColibriTool(ctx, 'server_ping', ...):
// +registerThoughtTools(ctx);

Expected 3-way merge collision: P0.2.4 (health tools) + P0.6.2 (skill_list) + this (thought tools) all add similar 1-line post-ping registrations. Sigma to resolve at merge time; packet minimizes the diff to exactly the registerThoughtTools(ctx) addition + the import.

3e. src/__tests__/server.test.ts (P0.2.1 — 1387 LOC)

  • Test pattern makeLinkedPair({ register }) — preconnect tool registration. For repository tests, I will reproduce this helper locally (or import via subrelative path if convenient). Each test gets a fresh in-memory DB + in-memory transport linked pair + a Client.
  • Tool response inspection via response.structuredContent as { ok, data }.

3f. src/db/migrations/001_init.sql

Empty migration slot; sets precedent that migration files carry a leading header comment block + SQL. 004 will follow the same style.

3g. docs/concepts/ζ-decision-trail.md

File does not exist. R75 Wave B spec gap — flagged by P0.2.2’s audit (per memory). docs/spec/s17-mcp-surface.md §1 does specify the tool list (thought_record, thought_list, audit_session_start, audit_verify_chain, merkle_finalize, merkle_root — 6 tools in Category 2). The spec names the Phase 0 tool thought_list, but the task-breakdown.md line 343 (canonical for this task) names it thought_record_list. Dispatch prompt locks thought_record_list. Packet does not attempt spec reconciliation — that is deferred (probable R76 docs cleanup).

3h. docs/spec/s17-mcp-surface.md §1 + §4 + §6

  • §1 Category 2: 6 tools including thought_record and thought_list (task-breakdown: thought_record_list).
  • §4 Middleware: 5-stage α chain. Registered via registerColibriTool.
  • §6 Response shape: uniform envelope {ok: true, data: ...} / {ok: false, error: {...}}. The wrapper handles this; handlers return the data payload.

3i. docs/reference/extractions/zeta-decision-trail-extraction.md (donor ref)

  • Donor algorithm: two-hash (content_hash + chain_hash). Colibri simplifies to one hash. P0.7.1 already implemented; P0.7.2 does not revisit.
  • Donor scope: session_id. Colibri Phase 0 analog: task_id (P0.7.1 contract §3: “scopes the chain to a task. Prevents cross-task forgery.”)
  • Donor thought_record tool signature: {session_id, type, content, parent_id?, metadata?}. Colibri’s Zod input is different (reflects task_id + agent_id + auto-id).
  • Donor chain was a tree (multi-child via shared parent_id). Colibri Phase 0 is strict linear chain via prev_hash (P0.7.1 audit §7 explicitly says “Phase-0 uses a strict linear chain via prev_hash”; donor branching is deferred to Phase 1+).

3j. docs/reference/mcp-tools-phase-0.md

Referenced by s17 as the per-tool catalogue. Not read in full — the task-breakdown.md line 343 is the canonical tool-name source.


§4. Chain scope resolution — per-task vs global

This is a load-bearing design question. The dispatch prompt §8 says:

“Default: prev_hash = the hash of the most recent thought_record in the ENTIRE table (global chain), or ZERO_HASH if the table is empty. Per-task chains are queried by filter but the underlying chain is global.”

However, the prompt qualifies: “Verify your interpretation in Step 1 Audit by reading P0.7.1 schema + audit/contract/packet docs.”

Reading P0.7.1:

P0.7.1 audit §7 (docs/audits/p0-7-1-trail-schema-audit.md):

“Colibri flattens task_id and agent_id to top-level fields and renames parent.chain_hashprev_hash… Phase-0 uses a strict linear chain via prev_hash.”

P0.7.1 contract §3 (docs/contracts/p0-7-1-trail-schema-contract.md):

“Chain integrity inputs:

  • task_id — scopes the chain to a task. Prevents cross-task forgery.”

This language — “scopes the chain to a task” and “prevents cross-task forgery” — explicitly endorses per-task chains. Cross-task forgery can only be prevented if each task has its own chain; a global chain would by construction interleave all tasks and have no “forgery” to prevent.

P0.7.3 dispatch prompt (same task-prompts file, lines 330-410): the verification tool test says “create 5 record chain, verify → valid=true” — singular chain, plural records, matches per-task scoping. And donor (which the extraction endorses) scoped by session_id; Phase 0 analog is task_id.

Decision: per-task chain. createThoughtRecord(input) resolves prev_hash as: the hash of the latest record WHERE task_id = input.task_id (ORDER BY created_at DESC LIMIT 1), or ZERO_HASH if no such record exists.

This is the P0.7.1-contracted interpretation and aligns with P0.7.3’s verifier logic. The prompt’s “global chain default” override is declined; the audit documents the deviation with reasoning.

4a. Implications

  • createThoughtRecord must query WHERE task_id = ? ORDER BY created_at DESC LIMIT 1 to fetch prev_hash.
  • Index strategy: (task_id, created_at) ASC is the natural index for both listThoughtRecords({task_id}) AND the latest-for-task lookup (scan backwards). This matches prompt §7’s baseline index.
  • listThoughtRecords({task_id, limit}) filters by task_id; if task_id is omitted, returns all records (matches spec). Order is ASC by created_at (insertion order, per spec: “returns chain in insertion order”).
  • An empty task_id filter is still valid in the listing method — returns interleaved records from all tasks. This may be useful for ops-level inspection but is NOT a “chain” at that point.

4b. agent_id default

Dispatch prompt §7 baseline shows agent_id TEXT (nullable per prompt’s English), but P0.7.1 Zod schema requires .min(1). Decision: agent_id is required, input-mandatory. Callers supply it. No sensible default exists (the repository doesn’t know what agent is calling). The MCP tool Zod schema requires it.


§5. thought_records table schema

Per dispatch prompt §7 baseline + P0.7.1 Zod constraints, the migration introduces:

CREATE TABLE thought_records (
  id          TEXT PRIMARY KEY,            -- UUID v4 via crypto.randomUUID()
  type        TEXT NOT NULL,               -- one of THOUGHT_TYPES
  task_id     TEXT NOT NULL,               -- required non-empty per P0.7.1 Zod
  agent_id    TEXT NOT NULL,               -- required non-empty per P0.7.1 Zod
  content     TEXT NOT NULL,               -- empty string allowed; NOT NULL
  timestamp   TEXT NOT NULL,               -- ISO-8601, included in hash
  prev_hash   TEXT NOT NULL,               -- 64 hex chars (ZERO_HASH for genesis)
  hash        TEXT NOT NULL UNIQUE,        -- 64 hex chars; UNIQUE enforces P0.7.3 tamper detection
  created_at  TEXT NOT NULL                -- ISO-8601 insertion-time (distinct from timestamp)
);
CREATE INDEX idx_trail_task ON thought_records(task_id, created_at);
CREATE INDEX idx_trail_prev ON thought_records(prev_hash);

Design notes:

  • hash UNIQUE — two records with identical hash are by construction identical (same 6 hash-input fields). A second insert with the same hash means either a hash collision (astronomically unlikely for SHA-256) or a duplicate attempt. UNIQUE catches the latter cleanly.
  • content NOT NULL — P0.7.1 schema allows empty string; SQLite '' is not NULL. Keep NOT NULL.
  • timestamp vs created_attimestamp is caller-supplied (may be set to any ISO-8601 string, is HASH-INCLUDED). created_at is insertion-time set by the repository (NEVER in the hash, ops metadata for ordering). Both ISO-8601; stable string sorting = stable time order.
  • idx_trail_task supports both listThoughtRecords({task_id}) ordered scans and the latest-for-task lookup.
  • idx_trail_prev — prompt §7 requested; used by P0.7.3 chain verifier to walk prev_hash → id links. Not strictly needed by P0.7.2 but shipping now saves P0.7.3 a migration.

All columns are TEXT — matches P0.7.1 schema (all string-typed). No CHECK constraints (avoid duplicating Zod; Zod is source of truth).


§6. Acceptance criteria mapping

Spec acceptance criteria (from docs/guides/implementation/task-breakdown.md §P0.7.2 lines 333-343):

Criterion Source Audit observation
createThoughtRecord(input) computes hash, links to previous L339 Repository function; input = {type, task_id, agent_id, content}. Generates id (UUID v4), timestamp (now ISO), resolves prev_hash (latest for task OR ZERO_HASH), computes hash via schema.computeHash, inserts in single transaction.
getThoughtRecord(id) returns record with hash L340 Repository SELECT * FROM thought_records WHERE id = ?. Returns ThoughtRecord shape or null.
listThoughtRecords({task_id?, limit?}) returns chain in insertion order L341 Repository SELECT * FROM thought_records WHERE (task_id = ? OR ?1 IS NULL) ORDER BY created_at ASC LIMIT ?. Maps rows to ThoughtRecord shape.
thought_record MCP tool: Zod input, returns record with computed hash L342 registerColibriTool(ctx, 'thought_record', {inputSchema: ...}, handler). Input schema: {type, task_id, agent_id, content} (no hash, no prev_hash — computed server-side). Output payload: the full ThoughtRecord.
thought_record_list MCP tool: returns chain for given task_id L343 registerColibriTool(ctx, 'thought_record_list', ...). Input: {task_id?, limit?}. Output: {records: ThoughtRecord[]}.

All 5 criteria have direct code paths.


§7. Test matrix scope (sketch — packet binds exact count)

Coverage target: ≥25 tests, 100% branch on repository.ts. Distribution plan:

describe Count Focus
createThoughtRecord 8 genesis (ZERO_HASH prev), chain link, auto-id, auto-timestamp, required fields, empty-string content, all 4 types, agent_id preservation
getThoughtRecord 4 returns shape, returns null on missing, cross-task retrieval, preserves hash exactly
listThoughtRecords 6 empty, task_id filter, no filter, limit, insertion-order (ASC), cross-task isolation
determinism 2 re-computed hash matches stored hash; fixed-input pinned hash (P0.7.1 snapshot)
thought_record MCP tool 3 envelope, 4-type parse, INVALID_PARAMS on bad input
thought_record_list MCP tool 3 envelope, filter by task_id, empty
registration hygiene 1 registers both tool names idempotently

Total: 27 tests. Packet may expand to 30.


§8. Parallel-wave collision check (Wave D)

Wave D dispatches four tasks in parallel:

Task Owner files Collision with P0.7.2?
P0.2.4 (health tools) src/server.ts (bootstrap edit), likely src/domains/system/* YES on src/server.ts bootstrap() — both add a registerXxxTools(ctx) line after server_ping. Trivial 3-way merge.
P0.3.2 (task CRUD) src/domains/tasks/repository.ts + src/db/migrations/002_tasks.sql + src/db/schema.sql + src/server.ts YES on src/server.ts bootstrap() + src/db/schema.sql (append). Migration numbers disjoint (002 vs 004).
P0.6.2 (skill CRUD + skill_list) src/domains/skills/repository.ts + src/db/migrations/003_skills.sql + src/db/schema.sql + src/server.ts YES on src/server.ts bootstrap() + src/db/schema.sql (append). Migration numbers disjoint (003 vs 004).
P0.7.2 (this task) src/domains/trail/repository.ts + src/db/migrations/004_thought_records.sql + src/db/schema.sql + src/server.ts + tests + docs n/a

Expected merge collisions:

  1. src/server.ts bootstrap — 4 tasks each add 1-2 lines. Sigma resolves via keep-all. Diffs are disjoint (different symbols, same file region).
  2. src/db/schema.sql — 3 tasks each append an ownership block at the bottom. Order of appends is append-insensitive (same content either way).

Not a collision: migration files (disjoint numbers), domain directories (disjoint).

Mitigation: keep this task’s src/server.ts diff to exactly one import + one call. Keep schema.sql diff to one block at EOF.


§9. Baseline verification

$ git rev-parse HEAD
6c26bb58  (P0.7.1 trail schema merged as PR #124)

$ git log -1 --format=%s
feat(p0-7-1): ζ hash-chained record schema — SHA-256 canonical JSON + ZERO_HASH genesis (#124)

$ node --version
v20.x (CI matrix, P0.1.3)

$ cat package.json | grep -E "zod|better-sqlite3|@modelcontextprotocol"
    "@modelcontextprotocol/sdk": "...",
    "better-sqlite3": "^11.5.0",
    "zod": "^3.23.8",

All P0.7.2 dependencies present:

  • P0.7.1 primitives (computeHash, ZERO_HASH, ThoughtType, ThoughtRecord) at src/domains/trail/schema.ts.
  • P0.2.2 DB init (src/db/index.ts) + migration runner.
  • P0.2.1 server (registerColibriTool) + P0.2.3 startup (Phase 2 DB open).
  • zod + better-sqlite3 + node:crypto already declared.

No new runtime dependencies required.


§10. Non-obvious gotchas

10a. exactOptionalPropertyTypes: true in tsconfig

TypeScript flag is ON. An optional field (limit?: number) CANNOT be assigned undefined explicitly — it must be omitted from the object literal. Handlers using {...base, ...(limit !== undefined ? {limit} : {})} pattern (as in server.ts line 372) must be replicated in repository result construction. Similarly, listThoughtRecords({task_id}) must not pass task_id: undefined into its internal filter spec.

10b. noUncheckedIndexedAccess: true

Array/object indexed access returns T | undefined. For db.prepare(...).get(id) this already returns T | undefined. For db.prepare(...).all(...) this returns unknown[]; cast to ThoughtRecord[] is still type-unsafe — prefer mapping rows one-by-one through a row decoder that narrows.

10c. crypto.randomUUID() uses

P0.7.1 contract §7 says: “No crypto.randomUUID() call. Callers supply id. P0.7.2 will generate.” Confirmed: P0.7.2 is where UUIDs enter. Use import { randomUUID } from 'node:crypto'.

10d. Timestamp generation

P0.7.1 contract §7 says: “No new Date().toISOString(). Callers supply timestamp. P0.7.2 will generate.” Confirmed: new Date().toISOString() in the repository. Inject a now() seam for test determinism.

10e. Test file path must be src/__tests__/domains/trail/repository.test.ts

Dispatch prompt locks this path. jest.config.ts roots: ['<rootDir>/src'] + testMatch: ['**/__tests__/**/*.test.ts'] discovers nested paths. Wave A-C used flat paths (src/__tests__/<name>.test.ts) for simpler modules; the nested layout is new in Wave D — validated by the test matcher, no jest.config change needed.

10f. Better-sqlite3 transaction + last-record-lookup

createThoughtRecord must be atomic: (a) SELECT latest for task, (b) compute hash, (c) INSERT. If steps (a) and (c) are in separate transactions, two concurrent inserts for the same task could both read the same “latest” and then both insert with the same prev_hash, producing a chain fork. Wrap in db.transaction(() => {...})(input) with BEGIN IMMEDIATE (or rely on the default which serializes writes on WAL). Better-sqlite3’s db.transaction() returns a function that runs the body inside a savepoint/transaction. For concurrent protection we rely on SQLite’s write serialization + the α tool-lock middleware (per-tool mutex). Both together are belt-and-suspenders.

Actually: α tool-lock alone serializes thought_record invocations — no two createThoughtRecord calls can race inside one process. For cross-process safety, the DB-level transaction is still needed (minor concern; Phase 0 is single-process stdio only). Ship the transaction wrapper.

10g. P0.7.1 schema UNIQUE + idempotent inserts

hash UNIQUE prevents duplicate-hash inserts. If a caller passes the same {type, task_id, agent_id, content, timestamp} twice AND the prev_hash is the same (same task, no prior inserts), the computed hash is identical → second insert fails. This is correct: the same 6-field subset IS the same record. The repository should propagate this error cleanly (Zod+SQLite error → HANDLER_ERROR envelope).

10h. Cross-worktree leak surveillance (Wave C feedback)

Step 1 git status was clean. During Step 4, before each commit, I will re-run git status and verify only expected files are staged. No known Wave D leak source (all 4 agents started from same main, no shared files beyond bootstrap lines).


§11. Exit criteria for Step 1

  • Target files catalogued as absent (§2).
  • P0.7.1 primitives + consumption contract documented (§3a, §3b).
  • DB + server integration points documented (§3c, §3d).
  • Chain scope = per-task, with rationale from P0.7.1 contract §3 (§4).
  • Table schema confirmed (§5).
  • All 5 acceptance criteria map to code paths (§6).
  • Test matrix sketched with ≥25 tests (§7).
  • Wave D collision points identified (§8).
  • Baseline + dependencies verified (§9).
  • Gotchas enumerated (§10).

Ready to proceed to Step 2 (contract).


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.