> ## Documentation Index
> Fetch the complete documentation index at: https://comis-feature-matrix-channel.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Contributing

> Development setup, coding standards, testing conventions, and PR process

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

<Steps>
  <Step title="Clone the repository">
    ```bash theme={null}
    git clone https://github.com/comis-platform/comis.git
    cd comis
    ```
  </Step>

  <Step title="Install dependencies">
    ```bash theme={null}
    pnpm install
    ```

    This installs all workspace packages including native dependencies (`better-sqlite3`, `sharp`).
  </Step>

  <Step title="Build all packages">
    ```bash theme={null}
    pnpm build
    ```

    Runs TypeScript compilation across all 15 packages, respecting project references.
  </Step>

  <Step title="Run tests">
    ```bash theme={null}
    pnpm test
    ```

    Runs all unit tests via the Vitest workspace configuration.
  </Step>

  <Step title="Run security lint">
    ```bash theme={null}
    pnpm lint:security
    ```

    Runs ESLint with `eslint-plugin-security` rules that enforce path safety, secret management, and other security constraints.
  </Step>
</Steps>

<Tip>
  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`.
</Tip>

## 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

| Element           | Convention        | Example                            |
| ----------------- | ----------------- | ---------------------------------- |
| Functions         | camelCase         | `createCircuitBreaker()`           |
| Types/Interfaces  | PascalCase        | `ChannelPort`, `NormalizedMessage` |
| Factory functions | `createXxx()`     | `createHookRunner()`               |
| Port interfaces   | `*Port` suffix    | `MemoryPort`, `SkillPort`          |
| Adapter classes   | `*Adapter` suffix | `SqliteMemoryAdapter`              |
| Constants         | SCREAMING\_SNAKE  | `MAX_RETRIES`                      |
| Files             | kebab-case.ts     | `hook-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 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:

```bash theme={null}
pnpm validate
# Expands to:
pnpm docs:check && pnpm build:clean && pnpm cycles && pnpm cycles:refs && pnpm lint:security && pnpm test:coverage
```

| Step                 | Command              | What It Checks                                                                                                   |
| -------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Docs compile         | `pnpm docs:check`    | Compiles every `docs/**/*.mdx` through the MDX compiler — catches syntax errors the Mintlify deploy would reject |
| Clean build          | `pnpm build:clean`   | `pnpm clean && pnpm build` — clean-room TypeScript build using project references (not incremental)              |
| Circular deps (dist) | `pnpm cycles`        | `madge` dist-mode check — finds circular dependencies between compiled packages                                  |
| Circular deps (refs) | `pnpm cycles:refs`   | `tsc -b --dry packages/comis` — catches project-reference cycles (TS6202)                                        |
| Security lint        | `pnpm lint:security` | ESLint security rules                                                                                            |
| Coverage             | `pnpm test:coverage` | `vitest 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](https://www.conventionalcommits.org/) 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:**

```bash theme={null}
pnpm validate
```

## Anti-Patterns

Common mistakes to avoid -- these are enforced by ESLint and will fail CI:

| Instead of                     | Use                       | Why                                        |
| ------------------------------ | ------------------------- | ------------------------------------------ |
| `path.join()`                  | `safePath()`              | SSRF and directory traversal protection    |
| `process.env.X`                | `SecretManager`           | Centralized secret management              |
| `throw new Error()`            | `return err(new Error())` | Result pattern for explicit error handling |
| Empty `.catch(() => {})`       | `suppressError()`         | Explicit error suppression                 |
| Cross-package internal imports | Public package exports    | Maintain package boundaries                |

<Warning>
  These patterns are enforced by ESLint. Your PR will not pass CI if it uses `path.join()`, `process.env`, or thrown exceptions.
</Warning>

## 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.

## Related

<CardGroup cols={2}>
  <Card title="Architecture" icon="hexagon" href="/developer-guide/architecture">
    Understand the codebase structure
  </Card>

  <Card title="Packages" icon="cubes" href="/developer-guide/packages">
    Package roles and dependency rules
  </Card>

  <Card title="Custom Adapters" icon="plug" href="/developer-guide/custom-adapters">
    Build your first adapter
  </Card>

  <Card title="Developer Guide" icon="code" href="/developer-guide/index">
    Back to developer guide overview
  </Card>
</CardGroup>
