Skip to main content
This page covers everything you need to start contributing to Comis: setting up the development environment, understanding the coding conventions, running tests, and submitting changes. For the complete engineering protocol, see AGENTS.md in the repository root.

Prerequisites

  • Node.js >= 22 — runtime and build toolchain
  • pnpm — package manager (monorepo workspace support)
  • Git — version control
  • Linux for deployment (macOS works for development only)

Setup

1

Clone the repository

2

Install dependencies

This installs all workspace packages including native dependencies (better-sqlite3, sharp).
3

Build all packages

Runs TypeScript compilation across all 15 packages, respecting project references.
4

Run tests

Runs all unit tests via the Vitest workspace configuration.
5

Run security lint

Runs ESLint with eslint-plugin-security rules that enforce path safety, secret management, and other security constraints.
Run pnpm validate before every push. This expands to the full gate chain: pnpm docs:check && pnpm build:clean && pnpm cycles && pnpm cycles:refs && pnpm lint:security && pnpm test:coverage.

Coding Standards

Principles

The codebase follows five mandatory engineering principles:
  • KISS — Prefer straightforward control flow over meta-programming
  • YAGNI — Do not add config keys, port methods, or feature flags without a concrete caller
  • DRY (Rule of Three) — Extract shared helpers only after three repetitions
  • Fail Fast — Return err() for unsupported or unsafe states via the Result pattern
  • Secure by Default — Security rules enforced at lint time

Naming Conventions

Key Rules

  • Return Result<T, E> — never throw exceptions. Use ok(), err(), tryCatch(), and fromPromise() from @comis/shared.
  • Use safePath() from @comis/core/security — never use path.join() (prevents directory traversal attacks).
  • Use SecretManager — never read process.env directly for secrets.
  • No eval() or Function() constructor — banned entirely.
  • No empty .catch(() => {}) — use suppressError() from @comis/shared.
  • ES modules only ("type": "module") with .js extensions in import paths.

Testing

  • Unit tests are co-located with source: component.ts alongside component.test.ts
  • Each package has its own vitest.config.ts with include: ["src/**/*.test.ts"]
  • Run a single package: cd packages/core && pnpm test
  • Run a single test file: pnpm vitest run src/path/to/file.test.ts
  • Integration tests live in test/integration/ and require pnpm build first (they import from dist/)
  • Integration tests run sequentially (maxConcurrency: 1, pool: "forks")
  • Run integration tests: pnpm test:integration
  • Run full E2E orchestration with log validation and JSON report: pnpm test:orchestrate
  • Run the live-fire real-provider test tier: pnpm test:live --dry (dry run, no cost) or pnpm test:live (requires COMIS_LIVE=1)
  • Run the connectivity sweep (all provider probes, one per integration): pnpm test:live sweep (requires COMIS_LIVE=1); filter with COMIS_LIVE_PROBES=llm-anthropic,llm-openai pnpm test:live sweep
  • Run the core-loop scenarios (LOOP-01..04): pnpm test:live loop (requires COMIS_LIVE=1 for real-LLM Stage-C assertions; Stage-A structural tests run without it); covers multi-turn.test.ts, tool-call.test.ts, restart.test.ts, streaming.test.ts
  • Run the LLM-cache scenarios (CACHE-01..03): pnpm test:live cache (requires COMIS_LIVE=1 for real-LLM Stage-C assertions; Stage-A structural tests run without it); covers anthropic-cache.test.ts, gemini-cache.test.ts, cache-matrix.test.ts
  • Run the context engine scenarios (CTX-01..05): pnpm test:live ctx (requires COMIS_LIVE=1 for real-LLM Stage-C assertions; Stage-A structural tests run without it); covers dag-invariants.test.ts, summarization.test.ts, expansion.test.ts, pipeline.test.ts
  • Run the long-term memory scenarios (MEM-01..08): pnpm test:live memory (requires COMIS_LIVE=1 for real-LLM Stage-C assertions; Stage-A structural and Stage-B local-embedding tests run without it); covers recall-golden.test.ts, embedding-matrix.test.ts, recall-lanes.test.ts, trust-safety.test.ts, consolidation.test.ts, cost-features.test.ts, budget-interaction.test.ts
  • Run the MCP transport/policy/trust scenarios (MCP-01..03): pnpm test:live mcp (requires COMIS_LIVE=1 for real-OAuth Stage-C; Stage-A structural and Stage-B mock-server tests run without it); covers transport-auth.test.ts, policy-ratelimit.test.ts, trust-sandbox.test.ts
  • Run the built-in tool invocation and mode scenarios (TOOL-01..02): pnpm test:live tools (requires COMIS_LIVE=1 for real-LLM Stage-C; Stage-A structural and Stage-B config-driven tests run without it); covers builtin-invoke.test.ts, modes.test.ts
  • Run the subagent orchestration scenarios (ORCH-01..04): pnpm test:live orch (requires COMIS_LIVE=1 for real-LLM Stage-C assertions; Stage-A structural and Stage-B config-driven tests run without it); covers dag-pipeline.test.ts, background-reentry.test.ts, routing.test.ts, isolation.test.ts
  • Run the media scenarios (MEDIA-01..04): pnpm test:live media (Stage-A/B run keyless — ffmpeg-absent text-fallback, STT fallback-chain routing, vision capability routing, and autoMode delivery decisions are deterministic and assert against the real product functions; Stage-C real STT/TTS/vision/image-gen round-trips require COMIS_LIVE=1 plus the matching provider keys, and ffmpeg for voice format conversion); covers voice-roundtrip.test.ts, fallback-chain.test.ts, vision.test.ts, image-gen.test.ts
  • Run the web/search/docs scenarios (WEB-01..03): pnpm test:live web (Stage-B runs keyless — document extraction of CSV/text within maxChars with the truncation marker, DOCX returns unsupported_mime via the default composite, wrapWebContent taint markers + marker-sanitization, and 8-provider search config-shape + per-provider key-gating + the freshness filter on the public schema are all deterministic and assert against the real product functions; Stage-C real per-provider judged answers require COMIS_LIVE=1 plus the matching search key, link fetching requires network egress, DuckDuckGo runs keyless with network, and PDF OCR fallback requires a vision key); covers search-providers.test.ts, link-understanding.test.ts, document-extraction.test.ts
  • Run the channel scenarios (CHAN-01..03): pnpm test:live channels (Stage-B runs keyless — the echo golden round-trip through the real channel registry, the empty-input credential-validation breadth across all 10 real channel adapters, the crash-safe SQLite delivery-queue crash-mid-delivery resume with a persistence-oracle integrity check, and the streaming/queue/overflow/dmScope config-shape against the real schemas are all deterministic and assert against the real product code; Stage-C real-account send-to-agent-to-reply round-trips, positive token validation, live Slack Socket Mode, and live IMAP/OAuth email require COMIS_LIVE=1 plus the channel’s credentials and are recorded manually in test/live/RUNBOOK.md); covers echo-golden.test.ts, delivery-modes.test.ts
  • Run the security and failure-injection scenarios (SEC-01..06): pnpm test:live sec (Stage-A/B run keyless — fault-injection degradation classified via the real classifyError, per-source prompt-injection neutralization through wrapExternalContent (taint markers + the suspicious-pattern callback) across the full ExternalContentSource union, validateMemoryWrite memory-poisoning classification (dangerous-command content blocked CRITICAL), the secret-residency scan (a planted canary positive control + the product redaction-to-residency chain + report/ledger), GatewayTokenSchema scope-disjointness (mcp-client co-issuance rejected) + the approval-gate pause/resolve state machine, and the macOS sandbox-exec deny-default exec-confinement are all deterministic and assert against the real product code; Stage-C real-provider failover under a real 429/5xx, the AgentDojo/ASB injection benchmark, the real-LLM redaction-on residency sweep, and the live-gateway admin-RPC-denial + rate-limit-429 require COMIS_LIVE=1 plus a real provider, and bwrap + net{open,broker-only} is SKIPPED(no-bwrap/linux-only) on macOS); covers failure-injection.test.ts, prompt-injection.test.ts, memory-poisoning.test.ts, secret-residency.test.ts, gateway-scopes.test.ts, sandbox-net.test.ts
  • Run the platform scenarios (PLAT-01..04): pnpm test:live plat (Stage-B runs keyless — the config system fail-fast (the closed ConfigError codes), layering precedence (defaults < envLayer < YAML), ${VAR} resolution, the isImmutableConfigPath truth-table (security.storage is immutable, restart-required), and the config-audit record + config:patched event shape; the 3 security.storage secrets backends (encrypted/file/env) each resolving a canary credential via selectSecretStore + the encrypted-no-SECRETS_MASTER_KEY fail-fast + the no-leak residency scan + the persistence oracle on secrets.db; the scheduler cron-fire (scheduler:job_started/scheduler:job_completed) + auto-suspend + concurrency cap + the execution.jsonl read-back + heartbeat ok/alert via injectable stubs; and the terminal-driver auto-answer/escalate-always/loop-guard/cap arithmetic + the terminal config-shape fail-fast are all deterministic and assert against the real product code; Stage-C the real-LLM-turn-from-cron, the live config.patch+restart+rollback over the gateway, and the real-boot credential auth require COMIS_LIVE=1 plus a real provider, and driving a real interactive CLI is SKIPPED(no-bwrap/linux-only) on macOS); covers config-system.test.ts, secrets-backends.test.ts, scheduler.test.ts, terminal-driver.test.ts
  • Run the end-to-end journey scenarios (E2E-01..05): pnpm test:live journeys (Stage-A/B run keyless — the extensible user-story library: a zod-validated UserStory schema (a malformed story rejects at parse — the schema is the executable acceptance spec), a self-registering STORY_LIBRARY (mirrors platform-tools/registry.ts), one generic journey-runner that interprets ANY story by its data, the open/closed zero-harness-change extensibility test (a synthetic story registered via registerStory alone auto-joins the library + the coverage view + the run grid with no runner/steps/schema edit), the step-vocabulary interpreter driving a story on the echo channel with dummy keys, the coverage auto-wiring view, the requires→skip-with-reason gating (an unmet provider/capability/platform/component-cert skips, never fails), and the 8 seed-story shapes US-01..08 are all deterministic and assert against the real product code; Stage-C/D the real-LLM multi-turn journey execution (goal-achieved + judged task-success + one stitched traceId + journey-level obs.billing, the N-run pass-rate × (scenario×model) grid) requires COMIS_LIVE=1 plus a real provider and the component Stage-C certs from the component scenario suites above, and J7 terminal-driven is SKIPPED(no-bwrap/linux-only) on macOS); covers types.test.ts, registry.test.ts, steps.test.ts, journey-runner.test.ts, coverage-wiring.test.ts, readiness-wiring.test.ts. Adding a journey = drop one declarative UserStory spec file under test/live/journeys/stories/ that calls registerStory(...) (imported from ./registry-core.js) + add one import line in registry.ts — zero change to the journey-runner, steps, or schema (open/closed). The new story automatically joins the next live run, the (scenario×model) grid, the coverage view, and READINESS.
  • Run the PROVE scenarios (PROVE-01..05): pnpm test:live prove (Stage-A/B run keyless — the observability meta-validation (PROVE-01: billed-tokens-equal-response-tokens cross-stream agreement + a turn reconstructed from the obs.trace session-index by traceId/messageId over a seeded index + no ERROR/WARN without hint+errorKind via runLogOracle — the observability oracle validating ITSELF), the cold-start tarball-bundle-mechanics contract (PROVE-02: the bundledDependencies @comis/* set the published tarball ships, every workspace package private:true) + the doctor/health finding-shape contract, and the short deterministic soak smoke (PROVE-03: the runSoak harness reuses the STORY_LIBRARY as traffic + parses the daemon “Daemon health” line for RSS/heap trend + zero stuckSubAgentRuns/deadLetterQueueSize/promptTimeoutsLast5m + empty degradedProviders) are all deterministic and assert against the real product code; the real multi-hour Linux-VPS soak is SKIPPED(operator), the full cold-start install→comis configure→boot→doctor/health green is SKIPPED(linux/validate:full), the real-provider full-run meta is SKIPPED(no-live), and judged real-key claims require COMIS_LIVE_JUDGE_* (skip otherwise); covers obs-meta.test.ts, cold-start.test.ts, soak-smoke.test.ts. The live tier runs under its own test/live/vitest.config.ts (fileParallelism:false) — daemon-booting scenarios run sequentially, so pnpm test:live is reliable WITHOUT the manual --no-file-parallelism flag. Generate the readiness summary with pnpm test:live --readiness (writes READINESS.md — honest PARTIAL on a keyless run, every category A–V + per-story US-01..08). A scheduled, budget-capped release gate (.github/workflows/live-fire.yml, weekly + manual dispatch) runs pnpm test:live all and publishes READINESS.md; it is operator-armed via repo secrets, never runs per-PR, and the pre-push hook stays mock-only — live never blocks a push.
  • Clean up integration test artifacts: pnpm test:cleanup

Tests-First Protocol

Every fix and every feature in packages/*/src/** starts with a failing test that fails on the pre-patch code, then a production patch that flips it to GREEN. Commit the test first when practical so the failing state is reproducible from that commit alone. Exempt from the tests-first rule: pure docs, comments, formatting, and build-tooling/CI/config edits — when in doubt, write the test.

RED → GREEN → REFACTOR

  1. RED — Write the failing test first. Run it and confirm it fails on the current codebase (pnpm vitest run src/your-file.test.ts exits non-zero). Commit the test-only change when it would compile against the current code.
  2. GREEN — Write the minimum production code to make the test pass. Run the test again and confirm it passes.
  3. REFACTOR (optional) — After GREEN, simplify if the patch leaves duplication or awkward seams. All tests must stay green.
What the test must prove: A test that passes both before and after the patch proves nothing — it must demonstrably FAIL on the pre-patch code. Commit ordering: Prefer landing the RED commit first (test-only, failing on current main) and the GREEN commit second. Combining is acceptable when the test would not compile against the pre-patch code, when the bug is too narrow to surface from a separate commit, or when shipping a security patch. Test types:
  • Bug fix → regression test that fails on the current codebase
  • New feature → contract test that pins the new behavior
  • Co-locate unit tests: src/component.ts + src/component.test.ts
  • Integration tests (daemon-level flows only): live in test/integration/

Validation Gates

Run pnpm validate before every push. This is the full gate chain:

Pre-Push Hook

A pre-push git hook (.githooks/pre-push) is auto-installed by the prepare script (git config core.hooksPath .githooks). It runs pnpm validate and blocks the push on failure. To bypass for a single push (e.g., docs-only changes): git push --no-verify The hook does not run integration, E2E, tarball, or audit tiers — those require Linux and additional system dependencies. Run pnpm validate:full on Linux for those.

Commits and Branches

Commit format: Conventional Commits with package scope:
Branch naming: Create branches from main using the pattern:
  • feature/<description> — new features
  • fix/<description> — bug fixes
  • docs/<description> — documentation changes
Validation before push:

Anti-Patterns

Common mistakes to avoid — these are enforced by ESLint and will fail CI:
These patterns are enforced by ESLint. Your PR will not pass CI if it uses path.join(), process.env, or thrown exceptions.

Risk Tiers

Changes are classified by risk level, which determines the review and testing requirements:
  • Low risk: Documentation, tests, type-only changes
  • Medium risk: Behavior changes in existing features
  • High risk: Security, ports, gateway, daemon, config, domain, and bootstrap changes
High-risk changes require extra review and thorough testing. When in doubt, treat a change as higher risk.

Architecture

Understand the codebase structure

Packages

Package roles and dependency rules

Custom Adapters

Build your first adapter

Developer Guide

Back to developer guide overview