Skip to main content
Instructions are always present in Charlie’s context. The entire instruction set is capped by a global token budget, so keep guidance tight and high‑signal.
If your guidance is a repeatable procedure for a specific task, prefer Playbooks. For a quick tour of all customization options, see the Customization overview.

What to include

  • Clear instructions: “Always/Prefer/Never” statements.
  • Concrete standards: file layout, naming, typing patterns; development commands; error handling; logging; dependency conventions; commit/PR norms.
  • Tiny Good/Bad examples (helpful but trimmed first under budget).
  • Glossary entries for repo‑specific terms and domain jargon.
  • Links to resources: URLs for repo paths with extended info (Note: links aren’t followed during PR review).
  • Directory‑scoped guidance where it truly differs (put AGENTS.md / CLAUDE.md / .cursor/rules/*.mdc next to the code).

What to exclude

  • Never: secrets, tokens, credentials.
  • Vague language like “should generally,” “consider” without a default. Use Always/Prefer/Never when X.
  • Philosophy, essays, or long rationale — link out to vendor docs instead.
  • Large code blocks, generated outputs, or changelogs.
  • One‑off “project diary” notes that won’t age well.
  • Duplicated guidance across files — keep one canonical instruction per topic.
  • Instructions that require capabilities you haven’t granted (e.g., “post to Slack” without Slack connected).

Style & structure

  • Voice: imperative, specific, short sentences, active verbs, concrete defaults.
  • Format: prefer Do/Don’t bullets over paragraphs.
  • Precision: name files, symbols, and scripts exactly; show the minimal command/snippet.
  • Organization: create a .charlie/instructions/*.md file for each topic (e.g. glossary, dev commands, TypeScript).
Use this template as a starting point when creating a new instruction file. It keeps rules concise and groups examples separately while allowing snippets to reference specific rules via stable IDs like [R1]. This is a suggestion only—use any subset of sections/formatting that fits your repo and delete anything that doesn’t apply.
# <Title>

<1–2 sentences on what this covers and why it matters.>

## Scope
<Paths and contexts where this applies (focus on globs, e.g., `apps/**`, `packages/*/src/**/*.ts`).>

## Context
- <Background or team‑specific nuance that clarifies rules where code alone isn’t clear.>
- <Assumptions/constraints decided by the team that Charlie wouldn’t infer from the repo or public docs.>

## Rules
- [R1] <Imperative rule, one line.>
- [R2] <Imperative rule, one line.>
- [R3] <Imperative rule, one line.>
  - Optional note for nuance (one bullet only).

## Examples

### Good examples

- [R1] — <short title for the snippet>
```ts
// minimal, runnable snippet for R1
```

- [R3] — <another good example>
```ts
// minimal, runnable snippet for R3
```

### Bad examples

- [R2] — <short title for the anti‑pattern>
```ts
// short anti‑pattern for R2
```

## References

1. <Short label><https://example.com/path-or-relative-doc>
2. <Short label> — ./relative/path.md
Tip: Keep the Rules list stable once published so example references like “R3” don’t drift. If you must reorder, update the example captions to match.

Example rule files

Use these copy‑ready examples to seed your own .charlie/instructions/*.md files. Paste them as‑is, then tweak to match your stack.

TypeScript

.charlie/instructions/core/typescript.md
# TypeScript Conventions

Keep TypeScript strict, modern, and ESM‑native so code is predictable and interoperable across tools.

## Scope
Applies to all TypeScript in this repo, especially `apps/**` and `packages/**` sources and tests.

## Context
- Strict type safety and avoiding use of `any` or unsafe casts is EXTREMELY important.
- The codebase uses pure ESM with NodeNext resolution. Relative imports must include the `.js` file extension even in `.ts` files to match runtime resolution.

## Rules
- [R1] ABSOLUTELY NEVER use `any` for ANYTHING other than tests/fixtures or generic default type parameters. Prefer `unknown` or `unknown[]` for generics.
- [R2] NEVER use non‑null assertions (`!`) and blanket casts (`as T`). Prefer `unknown` + Zod or type guards; use generics and `satisfies` to keep types precise.
- [R3] Use ESM `import`/`export` only. Never use `require()`/`module.exports`.
- [R4] In relative imports, include the `.js` suffix (NodeNext). Example: `import { foo } from './foo.js'` from a `.ts` file.
- [R5] Prefer named exports. Default exports are allowed only when a framework requires them (e.g., CLI command classes or framework route modules).
- [R6] Prefer unions over enums. Use `z.enum([...])` only for schemas solely consumed by `llm.generateObject()`.
- [R7] Use type‑only imports for types: `import type { Foo } from './types.js'`.
- [R8] Prefer discriminated unions for state and result types instead of boolean flags.
- [R9] Do not add custom `.d.ts` files to silence errors. Install missing `@types/*` or fix the import/module path.
- [R10] Use `const` by default; never use `var`.
- [R11] Use modern standard APIs by default (`fetch`, `URL`, `Blob`). Avoid large runtime deps for trivial tasks.

## Examples

> Note: In NodeNext ESM, include the `.js` suffix in relative imports even in `.ts` files.

### Good examples

- [R4][R5][R7][R8][R10] — Minimal ESM import + named export + type‑only import + discriminated union
```ts
import type { User } from './types.js'
type Result = { status: 'ok'; count: number } | { status: 'error'; reason: 'not-found' }
export const area = (r: number): number => Math.PI * square(r)
```

- [R2] — Type guard helper using `key in obj` for safe narrowing
```ts
type WithMessage = { message: unknown }
const hasMessage = (o: unknown): o is WithMessage =>
  typeof o === 'object' && o !== null && 'message' in o

export const getMessage = (e: unknown): string | undefined =>
  hasMessage(e) && typeof e.message === 'string' ? e.message : undefined
```

### Bad examples

- [R2][R5][R11] — Cast + Default export + using Axios instead of fetch
```ts
export default function getFoo(r: number) {
  const foo = await axios.get(`/foo/${r}`);
  return foo.data as Foo;
}
```

- [R1] — Using `any` to read `message` from `e: unknown` in a catch block
```ts
try {
  doThing()
} catch (e: unknown) {
  const msg: string = (e as any).message
}
```

## References
1. tsconfig.json — ./tsconfig.json
2. package.json — ./package.json

Glossary

A short glossary improves Charlie’s understanding and keeps terminology consistent.
.charlie/instructions/core/glossary.md
# Glossary

Canonical terms and capitalization used across this repo.

## Scope

Glossary terms are globally applicable.

## Entries

- **Charlie**: The AI agent that powers Charlie Labs.
- **Charlie Labs**: The company behind Charlie.
- **Devbox**: A containerized development environment Charlie uses to complete tasks.
- **Integration**: A connection between Charlie and a third-party platform (GitHub, Slack, Linear, etc.).
- **Customer**: A company that has an active subscription.
- **User**: An individual that works for a customer.

Git and PRs

These rules align branch names, commit messages, and PR titles/bodies with your repo’s conventions so Charlie opens clean branches, writes consistent commits, and prepares review‑ready PRs with the right labels and checks. Ensure to specify templates or styles that Charlie should follow.
.charlie/instructions/core/git-and-prs.md
# Git and PR Conventions

Keep history clean and reviews fast by standardizing branches, commits, and PRs. These rules help Charlie mirror your workflow and reduce back‑and‑forth in reviews.

## Scope
All git and GitHub actions.

## Context
- We use PR‑based development with forced squash‑merges.
- GitHub Actions are used for CI/CD and automated checks are required before merge.
- Linear is used for project management and automatically syncs with GitHub based on branch names, PR bodies, and commit messages.

## Rules
- [R1] Never push directly to the default branch (`main`/`master`). Create a branch and open a PR.
- [R2] Branch names: when a Linear issue ID exists, use `<issue-id>-<short-slug>` (e.g., `eng-123-add-onboarding-flow`); otherwise use a concise lowercase kebab‑case slug (e.g., `fix-typo-readme`).
- [R3] Keep branches focused: one logical change per PR. Avoid sweeping refactors and unrelated changes.
- [R4] Commit message subject is imperative, ≤72 chars, no emojis. Must follow conventional commits.
- [R5] Use a multi‑line commit body when context matters: what changed, why, and any follow‑ups. Wrap long lines (~100 chars max is fine).
- [R6] PR titles are concise (≤60 chars), no bracket tags or emojis.
  - Titles should follow Conventional Commits
  - Titles should end with the Linear issue ID when possible (e.g., `... (ENG-123)`)
- [R7] PR body includes: short Context/Motivation, Changes bullets, and Verification commands run locally.
  - The last line of the body should specify the issue ID with a keyword when applicable (e.g., `Resolves ENG-123`)
- [R8] Start PRs as Draft while WIP. Mark Ready only after local checks (lint/types/tests) pass and the description is accurate.
- [R9] Don’t rewrite public history on shared branches. Force‑push is OK on your own feature branch when rebasing.
- [R10] Linear issue references: put the issue ID in the branch name when applicable and reference it in commit/PR bodies (e.g., `Refs ENG-123`, `Closes #123`).

## References
1. Conventional Commits — https://www.conventionalcommits.org/en/v1.0.0/
2. GitHub Keywords — https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests

Dependencies

Tell Charlie how dependencies are managed and should be added/removed/upgraded.
.charlie/instructions/core/dependencies.md
# Dependencies

Rules for managing dependencies across the monorepo.

## Scope
All packages and apps in this monorepo.

## Context
- This is a monorepo that uses Bun with workspaces (apps/**, packages/**).
- Commmon dependency versions are specified in `workspaces.catalog` of the root `package.json`.
  - Catalog versions are used with `"catalog:"` syntax in workspace packages.
- Internal workspace packages are linked via `workspace:*` and expose ESM `exports`.

## Rules
- [R1] Use Bun only: `bun add`, `bun remove`, `bun update`. Do not use `npm`, `pnpm`, or `yarn`.
- [R2] Commit `package.json` and `bun.lock` together any time deps are added/removed.
- [R3] Never hand‑edit `bun.lock`. ALWAYS run `bun install` to update it.
- [R4] For internal deps, use `"workspace:*"` and never hardcode versions.
- [R5] Install `@types/*` when a package lacks types. Do not add `.d.ts` files to silence errors.
- [R6] Put type‑only or build/test tools in `devDependencies`; runtime imports belong in `dependencies`.

## References
1. Monorepo workspace config and catalog — package.json

CI & Verification

.charlie/instructions/core/ci-and-verification.md
# CI & Verification

Keep every PR green by running the same checks locally that CI runs, and by fixing verifier feedback instead of working around it.

## Scope
All packages and apps (`apps/**`, `packages/**`) and all PRs.

## Context
- CI runs on GitHub Actions and requires green checks before merge.
- CI verifies code with:
  - Vitest unit tests: `bun run test:all`
  - ESLint linting: `bun run lint:all` (`bun run fix:all` to auto‑fix)
  - TypeScript type checks: `bun run typecheck:all`
  - Prettier formatting: `bun run format:all` (`bun run fix:all` to auto‑fix)
  - Knip dead code & unused deps: `bun run knip`
  - Playwright end‑to‑end tests: `bun run test:e2e:all`
  - Note: the `*:all` scripts run for all packages/apps in the monorepo using `turbo`
- Verification scripts are run using Turborepo
  - Turborepo remote caching is enabled

## Rules
- [R1] Ensure tests, lint, formatting, types, and knip verification pass before opening a PR.
  - It's OK to have failing CI for draft/WIP/RFC PRs.
- [R2] NEVER bypass checks (`--no-verify`, `[skip ci]`, `git commit -n`). Fix the cause.
- [R3] Prefer `turbo` backed scripts because caching makes them faster and more reliable.
- [R4] Use `turbo --filter` to scope by package (`--filter=@acme/web`), directories (`--filter="./packages/utilities/*"`), changes from a branch (`--filter=[main...my-feature]`), etc. to run faster.
- [R5] When linting or formatting fail, try using the `--fix` flag to auto‑apply safe fixes first.
- [R6] NEVER change config files when tests/lint/format/etc fail. Fix the cause.

## References
1. Root verification scripts — package.json
2. Turborepo config — turbo.json
3. CI workflows — .github/workflows/

Coding Preferences

.charlie/instructions/core/coding-preferences.md
# Coding Preferences

Favor small, composable, side‑effect‑free code that is easy to test and maintain.

## Scope
All code in the repo.

## Context
- The agent loop benefits from predictable, pure helpers and clear seams for verifiers and tools.
- Our services are injected via `ctx.services.*`; avoid global singletons.

## Rules
- [R1] Prefer small pure functions over classes and shared mutable state.
- [R2] Use **early returns** to avoid deep nesting.
- [R3] Accept **options objects** for functions with >2 optional params; default and validate at the edge.
- [R4] Split modules when they exceed ~200 lines or contain unrelated concerns.
- [R5] Prefer dependency injection (pass functions/clients in) over reaching for globals.
- [R6] Avoid boolean flags in APIs; expose intent with discriminated unions (see TypeScript [R8]).
- [R7] Keep async boundaries explicit: return promises from helpers; do not hide `await` inside getters.
- [R8] Name things precisely: `parseFoo` (pure) vs `loadFoo` (I/O). Code must match the name.
- [R9] Prefer newer patterns/tools when the repo has different implementations of the same thing.

## Examples

### Good examples

- [R2][R3][R8] — Early return + options object
```ts
type CreateUserOpts = { sendWelcome?: boolean }
export function createUser(email: string, opts: CreateUserOpts = {}) {
  if (!email.includes('@')) return { status: 'error', reason: 'invalid-email' as const }
  const shouldWelcome = opts.sendWelcome === true
  // ...
  return { status: 'ok' as const }
}
```

### Bad examples

- [R6] — Ambiguous boolean flag
```ts
// What does "true" mean?
doThing('abc', true)
```

Docs & Comments

.charlie/instructions/core/docs-and-comments.md
# Docs & Comments

Document *why* decisions were made and how to use exported APIs. Keep comments short, accurate, and close to code.

## Scope
All code in the repo.

## Context
- JSDoc syntax (not TSDoc) is used for comments on exported functions/types.

## Comment Rules
- [R1] NEVER add comments that simply describe the code's behavior.
- [R2] NEVER add comments that describe a change that was made to the code.
- [R3] Every **exported** function/type must have a one‑line JSDoc summary and describes parameters/returns succinctly.
- [R4] Document **invariants and why**, not what the next line does.
- [R5] Use `TODO(OWNER|ISSUE-ID): <one‑line>` for actionable follow‑ups; prefer linking a Linear or GitHub issue.

## README Rules
- [R6] The repo is a private company repo, not open source; write READMEs accordingly.
- [R7] Keep README files to a tight “what/why/how/commands” shape; link out for deep refs.
- [R8] NEVER include licenses or other OSS-focused info in READMEs.
- [R9] Update docs and examples **in the same PR** that changes behavior.
- [R10] Prefer fenced code blocks with minimal, runnable snippets; avoid long, contrived examples.

## Examples

### Good examples

- [R3][R4] — JSDoc and invariants
```ts
/**
 * Parses a compact "a=b;c=d" cookie header.
 * @param {string} header - The cookie header to parse.
 * @returns {Array<[string, string]>} An array of key-value pairs parsed from the header.
 * @throws {Error} If the header is malformed.
 */
export function parseCookie(header: string): Array<[string, string]> { /* ... */ }
```

- [R5] — Actionable TODO
```ts
// TODO(ENG-123): Replace heuristic with API once v2 ships.
```

### Bad examples

- [R1] — Explaining what the code already says
```ts
// increment i by 1
i++
```

- [R2] — Describing a change to the code
```ts
// Add new function for parsing cookies
export function parseCookie(header: string): Array<[string, string]> { /* ... */ }
```

Errors & Logging

.charlie/instructions/core/errors-and-logging.md
# Errors & Logging

Use structured logging and typed errors. Capture context once; never leak secrets.

## Scope
All runtime code.

## Context
- We use a custom logger for compatibility with GCP logging.
- Sentry is used for error reporting and observability/tracing.

## Rules
- [R1] NEVER use `console.*`. Use `logger` (from `@acme/logger`) or `ctx.logger`.
- [R2] Log with **structured** context objects, then a short message. Include IDs, not secrets.
- [R3] Throw **Error** subclasses (or reuse existing ones). NEVER throw strings.
- [R4] Chain errors with `cause` to preserve stack/context.
- [R5] Keep log levels consistent: `debug` (noisy internals), `info` (major step), `warn` (recoverable), `error` (actionable failure).
- [R6] Do not log tokens, credentials, user content marked sensitive, or large payloads. Mask if you must include structure.

## Examples

### Good examples

- [R1][R2][R4] — Structured error logging with cause
```ts
try {
  await repo.ensurePullRequest()
} catch (err) {
  ctx.logger.error({ err, webhookEventId: ctx.meta.webhookEventId }, 'ensurePullRequest failed')
  throw new SupervisorRunError('Failed to ensure PR', { cause: forceError(err) })
}
```

### Bad examples

- [R1][R3] — Console + string throw
```ts
console.error('oops'); throw 'failed'
```

Testing

.charlie/instructions/core/testing.md
# Testing

Write fast, deterministic tests. Test logic, not integrations. Prefer unit tests; scope integration tests narrowly.

## Scope
All packages (`packages/**`) and app code with testable logic.

## Context
- We use Vitest for unit tests and Playwright for end‑to‑end tests.
- CI requires tests to pass with no flakiness.

## Rules
- [R1] Name tests `*.test.ts` and place under `__tests__/`.
- [R2] Tests must be hermetic: no real network, clock, or environment dependencies.
- [R3] Prefer snapshot and inline snapshot tests when possible.
- [R4] Prefer pure function tests over end‑to‑end. Add focused integration tests only where seams are stable.
- [R5] Use fake timers for time‑dependent code; reset state between tests.
- [R6] Do not depend on secrets. Use explicit stubs or env fallbacks in tests.

## Examples

### Good examples

- [R1][R2] — Minimal Vitest test
```ts
// foo.test.ts
import { expect, test } from 'vitest'
import { sum } from './foo.js'

test('sum adds small integers', () => {
  expect(sum(2, 3)).toBe(5)
})
```

- [R2] — Mocking fetch without hitting the network
```ts
import { expect, test } from 'vitest'
import { getUser } from './client.js'

test('client attaches bearer token', async () => {
  const orig = globalThis.fetch
  let calledInit: RequestInit | undefined
  globalThis.fetch = async (_u, init) => {
    calledInit = init; return new Response('{}', { status: 200 })
  }
  try { await getUser('u1') } finally { globalThis.fetch = orig }
  expect(calledInit?.headers).toMatchObject({ Authorization: expect.stringContaining('Bearer ') })
})
```

### Bad examples

- [R2] — Flaky sleep + network
```ts
test('loads data', async () => {
  await new Promise(r => setTimeout(r, 500))
  const res = await fetch('https://api.example.com/users') // real network
  // ...
})
```

Security & Privacy

.charlie/instructions/core/security-and-privacy.md
# Security & Privacy

Protect credentials and customer data by default. Minimize, gate, and sanitize all I/O.

## Scope
All code paths, especially anything that touches tokens, secrets, or user content.

## Context
- Bearer tokens are used for cross-service authentication.
- Sentry has automated PII and secret redaction.

## Rules
- [R1] Perform all external I/O via `ctx.services.*`. NEVER instantiate raw platform clients or call their HTTP endpoints directly.
- [R2] Data minimization: fetch and log only what you need. Redact tokens/PII in errors and logs.
- [R3] Never print or persist secrets. Do not echo env var values in service or CI logs.
- [R4] NEVER commit secrets or `.env` files. Keep `.env.example` with safe placeholders.
- [R5] Read env vars at **process edges** (boot/config layer) and pass down typed values. Do not call `process.env` deep inside helpers.

I