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

git clone https://github.com/comis-platform/comis.git
cd comis
2

Install dependencies

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

Build all packages

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

Run tests

pnpm test
Runs all unit tests via the Vitest workspace configuration.
5

Run security lint

pnpm lint:security
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

ElementConventionExample
FunctionscamelCasecreateCircuitBreaker()
Types/InterfacesPascalCaseChannelPort, NormalizedMessage
Factory functionscreateXxx()createHookRunner()
Port interfaces*Port suffixMemoryPort, SkillPort
Adapter classes*Adapter suffixSqliteMemoryAdapter
ConstantsSCREAMING_SNAKEMAX_RETRIES
Fileskebab-case.tshook-runner.ts

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 Phase 136 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 Phase 137 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 Phase 138 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 Phase 139 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 Phase 140 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 Phase 140 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 Phase 141 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 Phase 142 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 Phase 143 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 Phase 144 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 9 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 Phase 145 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 Phase 146 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 Phase 147 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 phases 136–146, 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 Phase 148 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 §5 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 now 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:
pnpm validate
# Expands to:
pnpm docs:check && pnpm build:clean && pnpm cycles && pnpm cycles:refs && pnpm lint:security && pnpm test:coverage
StepCommandWhat It Checks
Docs compilepnpm docs:checkCompiles every docs/**/*.mdx through the MDX compiler — catches syntax errors the Mintlify deploy would reject
Clean buildpnpm build:cleanpnpm clean && pnpm build — clean-room TypeScript build using project references (not incremental)
Circular deps (dist)pnpm cyclesmadge dist-mode check — finds circular dependencies between compiled packages
Circular deps (refs)pnpm cycles:refstsc -b --dry packages/comis — catches project-reference cycles (TS6202)
Security lintpnpm lint:securityESLint security rules
Coveragepnpm test:coveragevitest run --coverage with per-package coverage floors

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:
feat(agent): add retry logic for failed tool calls
fix(channels): handle WebSocket disconnect gracefully
test(memory): add FTS5 search edge cases
refactor(core): simplify hook runner priority sorting
chore(cli): update Commander.js to v13
docs(daemon): document pm2 setup process
Branch naming: Create branches from main using the pattern:
  • feature/<description> — new features
  • fix/<description> — bug fixes
  • docs/<description> — documentation changes
Validation before push:
pnpm validate

Anti-Patterns

Common mistakes to avoid — these are enforced by ESLint and will fail CI:
Instead ofUseWhy
path.join()safePath()SSRF and directory traversal protection
process.env.XSecretManagerCentralized secret management
throw new Error()return err(new Error())Result pattern for explicit error handling
Empty .catch(() => {})suppressError()Explicit error suppression
Cross-package internal importsPublic package exportsMaintain package boundaries
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