P0.2 — α System Core — Agent-Ready Task Prompts

This document contains copy-paste-ready prompts for agents executing the four P0.2 sub-tasks: MCP Server Bootstrap, SQLite Initialization, Two-Phase Startup, and Health Check Tool.

These tasks implement the core MCP server infrastructure and database layer that form the foundation of the Colibri runtime.

For the canonical task specifications, see task-breakdown.md § P0.2.

For extraction references (schema, startup patterns), see docs/reference/extractions/alpha-system-core-extraction.md and docs/reference/extractions/gamma-server-lifecycle-extraction.md (if available).


P0.2 Group Summary

Task ID Title Depends on Effort Unblocks Status
P0.2.1 MCP Server Bootstrap P0.1.2 M P0.2.3 spec-ready, code-not-started
P0.2.2 SQLite Initialization P0.1.4 L P0.2.3 spec-ready, code-not-started
P0.2.3 Two-Phase Startup P0.2.1, P0.2.2 M P0.2.4 spec-ready, code-not-started
P0.2.4 Health Check Tool P0.2.3 S (none in P0) spec-ready, code-not-started

P0.2.1 — MCP Server Bootstrap

Spec source: task-breakdown.md § P0.2.1

Extraction reference: alpha-system-core-extraction.md (server bootstrap section)

Worktree: feature/p0-2-1-mcp-bootstrap

Branch command:

git fetch origin
git worktree add .worktrees/claude/p0-2-1-mcp-bootstrap -b feature/p0-2-1-mcp-bootstrap origin/main
cd .worktrees/claude/p0-2-1-mcp-bootstrap

Estimated effort: Medium (M)

Depends on: P0.1.2 (Test Runner + Linter)

Unblocks: P0.2.3 (Two-Phase Startup)

Files to create

  • src/server.ts — MCP server initialization and tool registration
  • tests/server.test.ts — integration tests for server handshake and ping tool

Acceptance criteria (from spec)

  • McpServer created with name: "colibri", version from package.json
  • stdio transport onlyStdioServerTransport from @modelcontextprotocol/sdk. No HTTP transport in Phase 0 (S17 §2 — stdio is the only transport; HTTP belongs to a later phase)
  • Server exports registerTool(name, schema, handler) helper that wires every tool through the 5-stage middleware chain (α System Core): tool-lock → schema validate → audit enter → dispatch → audit exit
  • At least 1 registered tool: server/ping returns { status: "ok", version } and is exercised through all five middleware stages in an integration test
  • npm test passes with MCP handshake integration test
  • No AMS_* env vars are read — the donor namespace is not supported

Pre-flight reading (required before coding)

  1. CLAUDE.md — worktree rules
  2. docs/guides/implementation/task-breakdown.md § P0.2.1
  3. docs/reference/extractions/alpha-system-core-extraction.md (server bootstrap section)
  4. @modelcontextprotocol/sdk documentation (skim official MCP SDK docs)
  5. Your completed P0.1.1 + P0.1.2 commits (reference package.json)

Ready-to-paste agent prompt

You are a Phase 0 builder agent for Colibri.

Task: P0.2.1 — MCP Server Bootstrap

Your role: create an MCP server that listens on **stdio only** (S17 §2 — no HTTP in Phase 0), exposes a registerTool() helper
that wires every tool through the α five-stage middleware chain, and implements the server/ping tool as a proof-of-concept.

Required reading:
- CLAUDE.md (worktree rules)
- docs/guides/implementation/task-breakdown.md § P0.2.1
- docs/reference/extractions/alpha-system-core-extraction.md (server bootstrap section)
- Your P0.1.1 + P0.1.2 completions (package.json, jest.config.ts)

Setup:
1. Create or reuse worktree:
   git fetch origin
   git worktree add .worktrees/claude/p0-2-1-mcp-bootstrap -b feature/p0-2-1-mcp-bootstrap origin/main
   cd .worktrees/claude/p0-2-1-mcp-bootstrap

2. Create these files:
   - src/server.ts
   - tests/server.test.ts

src/server.ts structure:

Import from @modelcontextprotocol/sdk:
- McpServer
- StdioServerTransport

Import from your config (P0.1.4):
- config (for COLIBRI_DB_PATH, COLIBRI_LOG_LEVEL, COLIBRI_STARTUP_TIMEOUT_MS, version)

Import the α middleware chain (P0.2.4) or stub it if not yet landed:
- toolLock, validateSchema, auditEnter, dispatch, auditExit

Export:
- createServer() function
- registerTool() helper that wires every handler through the five-stage middleware chain
- A start() function that connects the stdio transport

Pseudocode:
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { config } from './config.js';
import { runMiddlewareChain } from './middleware/index.js'; // P0.2.4

const server = new McpServer({
  name: 'colibri',
  version: '0.0.1', // read from package.json
});

// stdio only — no HTTP in Phase 0 per S17 §2
const transport = new StdioServerTransport();

// registerTool wires every handler through the 5-stage middleware chain
export function registerTool(name, schema, handler) {
  server.tool(name, schema, async (args, extra) =>
    runMiddlewareChain({ name, schema, handler, args, extra })
  );
}

// Register the ping tool
registerTool('server/ping', z.object({}), async () => ({
  status: 'ok',
  version: '0.0.1',
}));

// Start
async function start() {
  await server.connect(transport);
  console.error('Colibri MCP server started on stdio');
}

export { server, start };

Key points:

  • stdio only. No HTTP, no WebSocket, no dashboard — S17 §2.
  • Every tool goes through the α five-stage chain. A tool that bypasses runMiddlewareChain is not a valid Phase 0 tool.
  • The ping tool is proof-of-concept; returns { status: "ok", version }.
  • server.connect(transport) is async; await it.
  • Log startup to console.error (stdout is reserved for JSON-RPC traffic on stdio).

Test file (tests/server.test.ts):

  • Test 1: Server initializes with name “colibri”
  • Test 2: registerTool adds tool to server (inspect server’s internal tools list)
  • Test 3: server/ping tool exists and returns { status: “ok”, version }
  • Test 4: MCP handshake completes (use a stdio test client from the SDK)
  • Test 5: Every tool call passes through all five α middleware stages (assert via audit records)
  • Test 6: Zod-rejected call is still audited at stage 3 with outcome: "schema_reject"

Example test structure:

describe('MCP Server', () => {
  it('should initialize with name colibri', () => {
    expect(server.name).toBe('colibri');
  });

  it('should have version from package.json', () => {
    expect(server.version).toBeDefined();
  });

  it('should register server/ping tool', () => {
    expect(server.tools.size).toBeGreaterThan(0);
  });

  it('server/ping should return status ok', async () => {
    const result = await server.tools.get('server/ping')?.handler({});
    expect(result).toEqual({ status: 'ok', version: expect.any(String) });
  });

  it('should use StdioServerTransport', () => {
    expect(transport).toBeInstanceOf(StdioServerTransport);
  });

  it('should audit every call enter + exit', async () => {
    await server.tools.get('server/ping')?.handler({});
    const rows = db.prepare('SELECT * FROM actions WHERE tool_name = ?').all('server/ping');
    expect(rows).toHaveLength(2); // enter + exit
  });
});

Acceptance criteria to verify:

  1. src/server.ts exists and imports from @modelcontextprotocol/sdk
  2. McpServer instance created with name “colibri”
  3. Version read from package.json (or hardcoded as 0.0.1)
  4. registerTool() helper exported and wires through the α five-stage middleware chain
  5. stdio transport only — StdioServerTransport is the only transport instantiated (S17 §2)
  6. server/ping tool registered and responds with { status, version }
  7. npm test passes (server.test.ts runs)
  8. Integration test confirms MCP handshake completes and middleware chain is exercised

Writeback: After completion:

  • task_update: task_id=”P0.2.1”, status=”done”, progress=100
  • thought_record: task_id=”P0.2.1”, branch=”feature/p0-2-1-mcp-bootstrap”, commit_sha=, tests_run="npm test (server.test.ts: handshake, ping tool, transport selection)", summary="MCP server initialized. Transport-agnostic (stdio/http). server/ping tool registered.", blockers="none"

Do NOT edit main checkout. Do NOT commit to main. Work only in feature worktree.

Next step: P0.2.2 (SQLite Initialization) is independent; P0.2.3 depends on both P0.2.1 and P0.2.2.


### Verification checklist

- [ ] `src/server.ts` imports McpServer and `StdioServerTransport` from the SDK (no HTTP transport import)
- [ ] McpServer instance has name "colibri"
- [ ] Version read from package.json
- [ ] `registerTool()` helper exported and wires every tool through the α five-stage middleware chain
- [ ] **stdio transport only** — no `StreamableHTTPServerTransport`, no `AMS_TRANSPORT`
- [ ] `server/ping` tool registered with empty Zod schema
- [ ] `server/ping` returns `{ status: "ok", version }` and produces linked `actions` enter+exit rows
- [ ] `tests/server.test.ts` covers handshake, ping, middleware-chain traversal, schema-reject auditing
- [ ] `npm test` passes

### Writeback template

```yaml
task_update:
  task_id: "P0.2.1"
  status: "done"
  progress: 100
  notes: |
    MCP server bootstrap complete.
    - McpServer created (name: colibri, version from package.json)
    - Transport: stdio only (StdioServerTransport) — no HTTP per S17 §2
    - registerTool() helper wires every handler through the α five-stage middleware chain
    - server/ping tool registered and audited enter+exit

thought_record:
  task_id: "P0.2.1"
  branch: "feature/p0-2-1-mcp-bootstrap"
  commit_sha: "..."
  tests_run: |
    npm test (server.test.ts: handshake, ping tool, transport)
  summary: |
    P0.2.1 complete. MCP server with stdio/http transport.
    registerTool() helper and server/ping proof-of-concept tool.
  blockers: "none"

Common gotchas

  1. Package.json version: Reading version dynamically from package.json can be tricky in ESM. Options:
    • Use import.meta.resolve() (Node 18.13+) to locate package.json
    • Hardcode as “0.0.1” for now, refine later
    • Use readFileSync() with path resolution
  2. Transport initialization: StdioServerTransport and StreamableHTTPServerTransport are initialized differently. Stdio is simple; HTTP needs host/port. Ensure config vars are set in .env or defaults.

  3. Async startup: server.connect(transport) is async. If you export a start() function, ensure tests await it. Don’t start the server in module scope; do it in a main() block in src/index.ts (created in P0.2.3).

  4. Tool schema format: MCP SDK tools take a Zod schema or plain object. For ping, an empty {} or z.object({}) is fine. Ensure schema imports are correct.

P0.2.2 — SQLite Initialization

Spec source: task-breakdown.md § P0.2.2

Extraction reference: alpha-system-core-extraction.md (schema section)

Worktree: feature/p0-2-2-sqlite-init

Branch command:

git fetch origin
git worktree add .worktrees/claude/p0-2-2-sqlite-init -b feature/p0-2-2-sqlite-init origin/main
cd .worktrees/claude/p0-2-2-sqlite-init

Estimated effort: Large (L)

Depends on: P0.1.4 (Environment Validation)

Unblocks: P0.2.3 (Two-Phase Startup)

Files to create

  • src/db/index.ts — SQLite initialization and database handle (P0.2.2)
  • src/db/schema.sqlempty schema header + the first migration slot only. Tables are earned per concept per docs/architecture/data-model.md §2; none of them land here
  • src/db/migrations/001_init.sql — the first migration; Phase 0 floor is just the schema header and pragmas
  • tests/db/init.test.ts — unit tests for DB init and the earning-rule behavior

Acceptance criteria (from spec)

  • Uses better-sqlite3 (sync API)
  • schema.sql ships an empty header; no tables are created here. Per-concept tables (β: tasks, task_transitions; ε: skills; ζ: thoughts, actions; η: merkle_nodes, merkle_roots; ν: sync_log) are added by their owning concept’s sub-task per the earning rule in docs/architecture/data-model.md §2
  • initDb(path) creates DB if not exists, runs migrations in order, returns Database instance
  • Idempotent: calling initDb() twice does not fail or duplicate data
  • WAL mode enabled: PRAGMA journal_mode=WAL
  • Foreign keys enabled: PRAGMA foreign_keys=ON
  • PRAGMA integrity_check runs once at startup; failure → refuse to boot
  • A failed migration halts boot with a clear error (α System Core spec §”Configuration”)
  • Test: fresh DB survives re-init with zero table count at the α floor; later concept tests add their own rows when they land
  • No “78 tables” target. Any PR that asserts a hard-coded table count is a scope drift

Pre-flight reading (required before coding)

  1. CLAUDE.md — worktree rules
  2. docs/guides/implementation/task-breakdown.md § P0.2.2
  3. docs/architecture/data-model.md §2 — the earning rule. No table lands on disk before the owning concept’s sub-task earns it.
  4. docs/concepts/α-system-core.md — data ownership section (α owns the file, not the schema)
  5. docs/reference/extractions/alpha-system-core-extraction.mdheritage reference only. The donor DDL is for algorithmic study, not direct copy-paste.
  6. better-sqlite3 documentation (skim for sync API, pragma usage)
  7. Your completed P0.1.4 commit (reference config structure)

Ready-to-paste agent prompt

You are a Phase 0 builder agent for Colibri.

Task: P0.2.2 — SQLite Initialization

Your role: implement a sync SQLite database layer that owns the file and the migration runner.
**You do not own the schema.** Phase 0 ships an empty schema header per the earning rule in
`docs/architecture/data-model.md` §2 — per-concept tables are added by their owning concept's
sub-task. Your job is idempotent init, WAL+FK pragmas, `integrity_check`, and ordered migration application.

Required reading:
- CLAUDE.md (worktree rules)
- docs/guides/implementation/task-breakdown.md § P0.2.2
- docs/architecture/data-model.md §2 (earning rule)
- docs/concepts/α-system-core.md (data ownership — α owns the file, not the schema)
- docs/reference/extractions/alpha-system-core-extraction.md (heritage reference only — do NOT copy-paste donor DDL)
- Your P0.1.4 completion (config structure)

Setup:
1. Create or reuse worktree:
   git fetch origin
   git worktree add .worktrees/claude/p0-2-2-sqlite-init -b feature/p0-2-2-sqlite-init origin/main
   cd .worktrees/claude/p0-2-2-sqlite-init

2. Create these files:
   - src/db/index.ts
   - src/db/schema.sql                    ← empty header + pragma setup, no tables
   - src/db/migrations/001_init.sql       ← empty migration (header comment only; α is table-free)
   - tests/db/init.test.ts

src/db/schema.sql structure:

The schema.sql file is **intentionally empty of tables**. Per the earning rule in
`docs/architecture/data-model.md` §2, α System Core owns the database **file** but not the schema.
Tables are earned by each concept's P0 sub-task:
- β (P0.3): `tasks`, `task_transitions`
- ε (P0.6): `skills`
- ζ (P0.7): `thoughts`, `actions`
- η (P0.8): `merkle_nodes`, `merkle_roots`
- ν (P0.9): `sync_log`

Each of those sub-tasks adds its own `migrations/NNN_<concept>.sql` file.

Key pragmas set from `src/db/index.ts` after `Database()` is constructed:
- `PRAGMA journal_mode = WAL`
- `PRAGMA foreign_keys = ON`
- `PRAGMA integrity_check`  → abort boot on failure

Example `schema.sql` contents:
```sql
-- Colibri α System Core — database bootstrap
-- α owns the file, not the schema. Tables are added per concept
-- by their owning P0 sub-task (see docs/architecture/data-model.md §2).
-- Pragmas are set from src/db/index.ts, not from SQL, so WAL takes effect
-- before any migration runs.

Example migrations/001_init.sql contents:

-- 001_init: α System Core — empty migration slot.
-- Reserved so later concept migrations start at 002+.

src/db/index.ts structure:

import Database from 'better-sqlite3';
import { readFileSync, readdirSync } from 'fs';
import { join } from 'path';
import { config } from '../config.js';

export function initDb(dbPath: string): Database.Database {
  const db = new Database(dbPath);

  // Pragmas — set BEFORE migrations run
  db.pragma('journal_mode = WAL');
  db.pragma('foreign_keys = ON');

  // Integrity check — abort boot on failure (α System Core spec)
  const integrity = db.pragma('integrity_check', { simple: true });
  if (integrity !== 'ok') {
    throw new Error(`Database integrity check failed: ${integrity}`);
  }

  // Run migrations in order. Each concept's sub-task adds its own NNN_<concept>.sql.
  const migrationsDir = join(import.meta.dirname, 'migrations');
  const files = readdirSync(migrationsDir)
    .filter(f => f.endsWith('.sql'))
    .sort();

  for (const file of files) {
    const sql = readFileSync(join(migrationsDir, file), 'utf-8');
    try {
      db.exec(sql);
    } catch (err) {
      throw new Error(`Migration ${file} failed: ${(err as Error).message}`);
    }
  }

  return db;
}

// Singleton handle — α owns it; no domain opens a raw handle of its own
let instance: Database.Database | null = null;

export function getDb(): Database.Database {
  if (!instance) {
    instance = initDb(config.COLIBRI_DB_PATH);
  }
  return instance;
}

export function closeDb(): void {
  if (instance) {
    instance.close();
    instance = null;
  }
}

Test file (tests/db/init.test.ts):

  • Test 1: initDb creates file if not exists
  • Test 2: initDb returns Database instance
  • Test 3: At the α floor, fresh DB has zero user tables (the earning rule; concept tables land in later sub-tasks)
  • Test 4: idempotent — calling initDb twice is safe (no error, no double-migration)
  • Test 5: foreign_keys pragma is ON
  • Test 6: journal_mode is WAL
  • Test 7: integrity_check passes on a fresh DB
  • Test 8: a deliberately corrupted DB file fails boot with a clear error

Example test structure:

describe('Database initialization', () => {
  const testDbPath = './test-colibri.db';

  afterEach(() => {
    if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath);
  });

  it('should create DB file if not exists', () => {
    initDb(testDbPath);
    expect(fs.existsSync(testDbPath)).toBe(true);
  });

  it('should have zero user tables at the α floor', () => {
    const db = initDb(testDbPath);
    const count = (db.prepare(
      "SELECT COUNT(*) as c FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
    ).get() as { c: number }).c;
    expect(count).toBe(0);
    db.close();
  });

  it('should be idempotent', () => {
    initDb(testDbPath).close();
    expect(() => initDb(testDbPath).close()).not.toThrow();
  });

  it('should have foreign_keys pragma ON', () => {
    const db = initDb(testDbPath);
    expect(db.pragma('foreign_keys', { simple: true })).toBe(1);
    db.close();
  });

  it('should have WAL mode enabled', () => {
    const db = initDb(testDbPath);
    expect(db.pragma('journal_mode', { simple: true })).toBe('wal');
    db.close();
  });
});

Acceptance criteria to verify:

  1. src/db/index.ts exists and imports better-sqlite3
  2. initDb(path) creates DB file if not exists
  3. schema.sql ships with an empty header — no CREATE TABLE statements
  4. migrations/001_init.sql ships empty; later concepts add 002_beta.sql, 003_epsilon.sql, etc.
  5. WAL pragma set (PRAGMA journal_mode=WAL)
  6. Foreign keys pragma set (PRAGMA foreign_keys=ON)
  7. PRAGMA integrity_check runs at startup and aborts boot on failure
  8. Fresh DB has zero user tables at the α floor — no “78 tables” target
  9. Calling initDb twice is safe (no duplicate migrations)
  10. npm test passes (all db tests pass)

Writeback: After completion:

  • task_update: task_id=”P0.2.2”, status=”done”, progress=100
  • thought_record: task_id=”P0.2.2”, branch=”feature/p0-2-2-sqlite-init”, commit_sha=, tests_run="npm test (db.init.test.ts: create, zero-table floor, idempotent, pragmas, integrity_check)", summary="SQLite DB layer: better-sqlite3 WAL+FK, integrity_check at boot, ordered migrations. α owns the file; schema is earned per concept.", blockers="none"

Do NOT edit main checkout. Do NOT commit to main. Work only in feature worktree.

Next step: P0.2.3 (Two-Phase Startup) depends on both P0.2.1 and P0.2.2.


### Verification checklist

- [ ] `src/db/schema.sql` exists with an **empty header only** (no CREATE TABLE)
- [ ] `src/db/migrations/001_init.sql` exists as an empty placeholder
- [ ] Pragmas set from `src/db/index.ts`: `journal_mode=WAL`, `foreign_keys=ON`, `integrity_check`
- [ ] `src/db/index.ts` imports better-sqlite3
- [ ] `initDb(path)` function creates DB file if not exists
- [ ] `initDb` returns Database instance after running ordered migrations
- [ ] `getDb()` and `closeDb()` singleton exported; no domain opens a raw handle
- [ ] **Fresh DB has zero user tables at the α floor** (per `data/colibri.db` earning rule)
- [ ] Calling `initDb` twice is safe (no error, no double-migration)
- [ ] `tests/db/init.test.ts` covers all criteria
- [ ] `npm test` passes

### Writeback template

```yaml
task_update:
  task_id: "P0.2.2"
  status: "done"
  progress: 100
  notes: |
    SQLite database layer initialized.
    - Empty schema header; per-concept tables earned per data-model.md §2
    - WAL mode enabled
    - Foreign keys enforced
    - PRAGMA integrity_check at boot (fail fast)
    - Ordered migration runner

thought_record:
  task_id: "P0.2.2"
  branch: "feature/p0-2-2-sqlite-init"
  commit_sha: "..."
  tests_run: |
    npm test (db.init.test.ts: create, zero-table floor, idempotent, pragmas, integrity_check)
  summary: |
    P0.2.2 complete. α owns the database file, not the schema.
    WAL + FK + integrity_check. Ordered migration runner. Singleton handle via getDb().
    Zero user tables at the α floor; later concepts add their own migrations.
  blockers: "none"

Common gotchas

  1. Do not copy donor DDL. The donor alpha-system-core-extraction.md contains 78 tables of heritage schema. None of it lands in Phase 0 α. Copy-pasting it is a scope-drift bug; reviewers will reject the PR.

  2. Pragmas first. Set PRAGMA journal_mode=WAL and PRAGMA foreign_keys=ON from src/db/index.ts immediately after opening the handle, before any migration runs. integrity_check comes next.

  3. import.meta.dirname: Node 20.11+ syntax. For older Node, use fileURLToPath(import.meta.url) + dirname().

  4. Test cleanup: better-sqlite3 holds file locks. Close the handle in afterEach or tests will leak file descriptors.

  5. Table count assertions are banned. Do not write a test that asserts a hard-coded table count. Each concept asserts its own tables when it lands.


P0.2.3 — Two-Phase Startup

Spec source: task-breakdown.md § P0.2.3

Extraction reference: alpha-system-core-extraction.md (startup section), gamma-server-lifecycle-extraction.md (if available)

Worktree: feature/p0-2-3-two-phase-startup

Branch command:

git fetch origin
git worktree add .worktrees/claude/p0-2-3-two-phase-startup -b feature/p0-2-3-two-phase-startup origin/main
cd .worktrees/claude/p0-2-3-two-phase-startup

Estimated effort: Medium (M)

Depends on: P0.2.1 (MCP Server Bootstrap), P0.2.2 (SQLite Initialization)

Unblocks: P0.2.4 (Health Check Tool)

Files to create

  • src/startup.ts — two-phase startup orchestration
  • src/index.ts — entry point that calls startup()
  • tests/startup.test.ts — unit tests for phase 1/2, graceful shutdown

Acceptance criteria (from spec)

  • Phase 1 (transport): MCP transport ready, health check responds, DB not yet loaded
  • Phase 2 (heavy init): DB initialized, all tools registered, all domains loaded
  • startup() returns only after Phase 2 completes
  • If Phase 2 fails, server shuts down gracefully (no hanging process)
  • Startup time logged: console.error("Startup complete in {ms}ms")
  • Test: mock Phase 2 failure → verify clean shutdown

Pre-flight reading (required before coding)

  1. CLAUDE.md — worktree rules
  2. docs/guides/implementation/task-breakdown.md § P0.2.3
  3. docs/reference/extractions/alpha-system-core-extraction.md (startup section)
  4. Your completed P0.2.1 + P0.2.2 commits (reference server and db modules)

Ready-to-paste agent prompt

You are a Phase 0 builder agent for Colibri.

Task: P0.2.3 — Two-Phase Startup

Your role: orchestrate startup in two phases:
  Phase 1 (fast): transport ready, health check responds, DB not loaded
  Phase 2 (slow): DB initialized, all tools registered

Required reading:
- CLAUDE.md (worktree rules)
- docs/guides/implementation/task-breakdown.md § P0.2.3
- docs/reference/extractions/alpha-system-core-extraction.md (startup section)
- Your P0.2.1 (server.ts) and P0.2.2 (db/index.ts) completions

Setup:
1. Create or reuse worktree:
   git fetch origin
   git worktree add .worktrees/claude/p0-2-3-two-phase-startup -b feature/p0-2-3-two-phase-startup origin/main
   cd .worktrees/claude/p0-2-3-two-phase-startup

2. Create these files:
   - src/startup.ts
   - src/index.ts (entry point)
   - tests/startup.test.ts

src/startup.ts structure:

```typescript
import { server, registerTool, start as startServer } from './server.js';
import { initDb, closeDb } from './db/index.js';
import { config } from './config.js';

enum StartupPhase {
  IDLE = 'idle',
  PHASE_1 = 'phase1',
  PHASE_2 = 'phase2',
}

let currentPhase = StartupPhase.IDLE;

async function startup(): Promise<void> {
  const startTime = Date.now();

  try {
    // PHASE 1: Start transport, enable health check
    console.error('[Startup] Phase 1: Starting transport...');
    currentPhase = StartupPhase.PHASE_1;
    await startServer(); // Assumes startServer() connects transport
    console.error('[Startup] Phase 1 complete: transport ready');

    // PHASE 2: Heavy initialization
    console.error('[Startup] Phase 2: Initializing database and tools...');
    currentPhase = StartupPhase.PHASE_2;

    // Initialize DB
    const dbPath = config.DATABASE_PATH || './data/ams.db';
    const db = initDb(dbPath);
    console.error('[Startup] Database initialized');

    // Register additional tools (e.g., server/health in P0.2.4)
    // For now, just the ping tool from P0.2.1
    console.error('[Startup] Tools registered');

    // Phase 2 complete
    const elapsed = Date.now() - startTime;
    console.error(`[Startup] Startup complete in ${elapsed}ms`);
  } catch (error) {
    console.error(`[Startup] Phase 2 failed, graceful shutdown:`, error);
    await gracefulShutdown();
    process.exit(1);
  }
}

async function gracefulShutdown(): Promise<void> {
  console.error('[Shutdown] Closing database...');
  closeDb();
  console.error('[Shutdown] Database closed');
  // Could also close server/transport here if needed
}

export { startup, currentPhase, StartupPhase, gracefulShutdown };

src/index.ts (entry point):

import { startup } from './startup.js';

async function main() {
  try {
    await startup();
    // After startup, server is running and listening
    // Process runs indefinitely until SIGINT/SIGTERM
  } catch (error) {
    console.error('Fatal error during startup:', error);
    process.exit(1);
  }
}

main();

Test file (tests/startup.test.ts):

  • Test 1: startup() completes successfully
  • Test 2: currentPhase transitions: IDLE → PHASE_1 → PHASE_2
  • Test 3: startup() returns only after Phase 2 completes
  • Test 4: if Phase 2 fails (mock error), gracefulShutdown is called
  • Test 5: startup time is logged

Example test structure:

describe('Startup', () => {
  it('should transition through phases', async () => {
    // Mock server.start(), db.initDb()
    jest.mock('./server.js');
    jest.mock('./db/index.js');

    await startup();
    expect(currentPhase).toBe(StartupPhase.PHASE_2);
  });

  it('should log startup time', async () => {
    const consoleSpy = jest.spyOn(console, 'error');
    await startup();
    expect(consoleSpy).toHaveBeenCalledWith(
      expect.stringMatching(/Startup complete in \d+ms/)
    );
  });

  it('should gracefully shutdown if Phase 2 fails', async () => {
    jest.mock('./server.js');
    jest.mock('./db/index.js', () => ({
      initDb: jest.fn().mockRejectedValue(new Error('DB init failed')),
      closeDb: jest.fn(),
    }));

    await expect(startup()).rejects.toThrow();
    expect(closeDb).toHaveBeenCalled();
  });
});

Acceptance criteria to verify:

  1. src/startup.ts defines Phase 1 and Phase 2
  2. Phase 1: startServer() called, transport ready
  3. Phase 2: initDb() called, tools registered
  4. startup() returns only after Phase 2 completes
  5. Startup time logged with elapsed ms
  6. If Phase 2 fails, gracefulShutdown() called and process exits
  7. currentPhase exported and trackable
  8. src/index.ts calls startup() and handles errors
  9. npm test passes (startup.test.ts covers phase transitions)

Writeback: After completion:

  • task_update: task_id=”P0.2.3”, status=”done”, progress=100
  • thought_record: task_id=”P0.2.3”, branch=”feature/p0-2-3-two-phase-startup”, commit_sha=, tests_run="npm test (startup.test.ts: phase transitions, time logging, shutdown)", summary="Two-phase startup: Phase 1 (transport), Phase 2 (DB+tools). Graceful shutdown on error.", blockers="none"

Do NOT edit main checkout. Do NOT commit to main. Work only in feature worktree.

Next step: P0.2.4 (Health Check Tool) depends on your completion.


### Verification checklist

- [ ] `src/startup.ts` defines StartupPhase enum (IDLE, PHASE_1, PHASE_2)
- [ ] currentPhase exported and transitions correctly
- [ ] Phase 1: startServer() called
- [ ] Phase 2: initDb() called, tools registered
- [ ] startup() is async and returns Promise<void>
- [ ] Startup time logged with elapsed milliseconds
- [ ] gracefulShutdown() called if Phase 2 fails
- [ ] Process exits with code 1 on failure
- [ ] `src/index.ts` exists and calls startup() on main
- [ ] tests/startup.test.ts covers phase transitions and shutdown
- [ ] npm test passes

### Writeback template

```yaml
task_update:
  task_id: "P0.2.3"
  status: "done"
  progress: 100
  notes: |
    Two-phase startup orchestration complete.
    - Phase 1: transport ready
    - Phase 2: DB + tools initialized
    - Graceful shutdown on error
    - Startup time logged

thought_record:
  task_id: "P0.2.3"
  branch: "feature/p0-2-3-two-phase-startup"
  commit_sha: "..."
  tests_run: |
    npm test (startup.test.ts: phase transitions, time, shutdown)
  summary: |
    P0.2.3 complete. Two-phase startup with orchestration.
    Phase 1 fast (transport), Phase 2 heavy (DB+tools).
    Graceful shutdown on Phase 2 failure.
  blockers: "none"

Common gotchas

  1. Async/await: startup() must be async and return Promise. Ensure all await calls are in place (startServer, initDb, etc.). Missing await = race condition.

  2. Phase transition timing: Phase 2 must NOT complete until DB is fully initialized. If you parallelize DB init with tool registration, ensure DB is ready BEFORE registration (order matters).

  3. Shutdown signal handling: For P0.2, gracefulShutdown is called on error. Later phases may add SIGINT/SIGTERM handlers. Keep it simple for now.

  4. Timing accuracy: The spec says “startup time logged”. Use Date.now() at start and calculate elapsed = Date.now() - startTime. Console.error is correct (stdout for logs, stderr for server events).


P0.2.4 — Health Check Tool

Spec source: task-breakdown.md § P0.2.4

Worktree: feature/p0-2-4-health-tool

Branch command:

git fetch origin
git worktree add .worktrees/claude/p0-2-4-health-tool -b feature/p0-2-4-health-tool origin/main
cd .worktrees/claude/p0-2-4-health-tool

Estimated effort: Small (S)

Depends on: P0.2.3 (Two-Phase Startup)

Unblocks: (none in Phase 0)

Files to create

  • src/tools/health.ts — health check tool implementation
  • tests/tools/health.test.ts — unit tests for health check response

Acceptance criteria (from spec)

  • Tool name: server/health
  • Returns: { status, version, uptime_ms, db_tables, phase, mode }
  • db_tables: count of SQLite tables (verifies schema loaded)
  • [ ] phase: "phase1" "phase2"
  • mode: current runtime mode string
  • Response time < 100ms

Pre-flight reading (required before coding)

  1. CLAUDE.md — worktree rules
  2. docs/guides/implementation/task-breakdown.md § P0.2.4
  3. Your completed P0.2.1 (server.registerTool), P0.2.3 (startup, currentPhase)

Ready-to-paste agent prompt

You are a Phase 0 builder agent for Colibri.

Task: P0.2.4 — Health Check Tool

Your role: implement a health check MCP tool that reports server status, uptime, DB state, and current phase.

Required reading:
- CLAUDE.md (worktree rules)
- docs/guides/implementation/task-breakdown.md § P0.2.4
- Your P0.2.1 (server.registerTool), P0.2.3 (startup phases)

Setup:
1. Create or reuse worktree:
   git fetch origin
   git worktree add .worktrees/claude/p0-2-4-health-tool -b feature/p0-2-4-health-tool origin/main
   cd .worktrees/claude/p0-2-4-health-tool

2. Create these files:
   - src/tools/health.ts
   - tests/tools/health.test.ts

src/tools/health.ts structure:

```typescript
import { getDb } from '../db/index.js';
import { currentPhase } from '../startup.js';
import { config } from '../config.js';

const startTime = Date.now(); // Track server uptime

export interface HealthResponse {
  status: string;
  version: string;
  uptime_ms: number;
  db_tables: number;
  phase: 'phase1' | 'phase2';
  mode: string;
}

export async function healthCheck(): Promise<HealthResponse> {
  const startCheck = Date.now();

  // Get DB table count
  let dbTables = 0;
  try {
    const db = getDb();
    const result = db.prepare(
      "SELECT COUNT(*) as count FROM sqlite_master WHERE type='table'"
    ).get() as { count: number };
    dbTables = result.count;
  } catch (err) {
    // DB not ready, count = 0
    dbTables = 0;
  }

  const elapsed = Date.now() - startCheck;

  return {
    status: 'ok',
    version: '0.0.1', // from package.json
    uptime_ms: Date.now() - startTime,
    db_tables: dbTables,
    phase: currentPhase as 'phase1' | 'phase2',
    mode: config.NODE_ENV || 'unknown',
  };
}

Integration in src/server.ts (or startup.ts):

Register the health tool in the server:

import { healthCheck } from './tools/health.js';

registerTool('server/health', {}, async () => {
  return await healthCheck();
});

Test file (tests/tools/health.test.ts):

  • Test 1: healthCheck() returns expected fields
  • Test 2: Response time < 100ms
  • Test 3: db_tables > 0 when DB initialized
  • Test 4: phase reflects current startup phase
  • Test 5: status is “ok”
  • Test 6: version matches package.json

Example test structure:

describe('Health Check Tool', () => {
  it('should return all required fields', async () => {
    const result = await healthCheck();
    expect(result).toHaveProperty('status');
    expect(result).toHaveProperty('version');
    expect(result).toHaveProperty('uptime_ms');
    expect(result).toHaveProperty('db_tables');
    expect(result).toHaveProperty('phase');
    expect(result).toHaveProperty('mode');
  });

  it('should return status ok', async () => {
    const result = await healthCheck();
    expect(result.status).toBe('ok');
  });

  it('should respond in < 100ms', async () => {
    const start = Date.now();
    await healthCheck();
    const elapsed = Date.now() - start;
    expect(elapsed).toBeLessThan(100);
  });

  it('should report correct phase', async () => {
    // Mock startup phase
    const result = await healthCheck();
    expect(['phase1', 'phase2']).toContain(result.phase);
  });

  it('should count DB tables', async () => {
    // Requires P0.2.2 to be complete
    const result = await healthCheck();
    expect(result.db_tables).toBeGreaterThan(0);
  });
});

Acceptance criteria to verify:

  1. src/tools/health.ts exists and exports healthCheck()
  2. healthCheck() returns object with all required fields
  3. status = “ok”
  4. version from package.json (or 0.0.1)
  5. uptime_ms is a number > 0
  6. db_tables is a number (count from sqlite_master)
  7. phase is “phase1” or “phase2”
  8. mode is NODE_ENV value
  9. Response time < 100ms
  10. healthCheck() registered as server/health tool
  11. npm test passes

Writeback: After completion:

  • task_update: task_id=”P0.2.4”, status=”done”, progress=100
  • thought_record: task_id=”P0.2.4”, branch=”feature/p0-2-4-health-tool”, commit_sha=, tests_run="npm test (health.test.ts: fields, timing, phase, db_tables)", summary="Health check tool (server/health) reports status, version, uptime, DB tables, phase, mode.", blockers="none"

Do NOT edit main checkout. Do NOT commit to main. Work only in feature worktree.

Next step: Phase 0 continues with P0.3 (β Task Pipeline). See task-breakdown.md for P0.3 specs.


### Verification checklist

- [ ] `src/tools/health.ts` exists and exports healthCheck()
- [ ] Returns object with all 6 fields: status, version, uptime_ms, db_tables, phase, mode
- [ ] status = "ok"
- [ ] version defined (0.0.1 or from package.json)
- [ ] uptime_ms is number representing ms since server start
- [ ] db_tables is number from `SELECT COUNT(*) FROM sqlite_master WHERE type='table'`
- [ ] phase is "phase1" or "phase2" (from currentPhase)
- [ ] mode is NODE_ENV value
- [ ] Response time < 100ms (no slow operations)
- [ ] Registered as tool "server/health" with empty schema
- [ ] tests/tools/health.test.ts covers all criteria
- [ ] npm test passes

### Writeback template

```yaml
task_update:
  task_id: "P0.2.4"
  status: "done"
  progress: 100
  notes: |
    Health check tool implemented.
    - Tool name: server/health
    - Response < 100ms
    - Reports: status, version, uptime, DB tables, phase, mode

thought_record:
  task_id: "P0.2.4"
  branch: "feature/p0-2-4-health-tool"
  commit_sha: "..."
  tests_run: |
    npm test (health.test.ts: fields, timing, phase, DB count)
  summary: |
    P0.2.4 complete. Health check tool with all required fields.
    Reports server status, uptime, DB table count, startup phase, runtime mode.
  blockers: "none"

Common gotchas

  1. Uptime tracking: Use a module-level startTime = Date.now() so uptime is relative to server startup, not tool registration. Calling healthCheck multiple times should show increasing uptime.

  2. DB access: If DB is not initialized, catching the error and returning db_tables=0 is correct. Don’t crash the health check on DB unavailability.

  3. Phase typing: currentPhase is an enum. Cast it to 'phase1' | 'phase2' to satisfy TypeScript. Ensure import is from startup.js.

  4. Response time: The spec says < 100ms. This is almost always achieved for a simple query, but avoid expensive operations in healthCheck() (e.g., don’t query all tables, just count them).


Previous group: P0.1 — Project Infrastructure — see p0.1-infrastructure.md

Next phase: P0.3 — β Task Pipeline (when ready, see task-breakdown.md)

Back to index: task-breakdown.md

Master bootstrap: agent-bootstrap.md (required reading for all Phase 0 agents)


Generated for Colibri R69. Last updated: 2026-04-09


Back to top

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

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