AGENTS.md

July 3, 2026 · View on GitHub

Identity

Linearis is a CLI tool for Linear.app that outputs JSON only (except help/usage text). It is designed for both human users and LLM agents, optimized for minimal token usage and maximum structure.

  • Runtime: Node.js ≥ 22, ES Modules, TypeScript strict mode
  • Package manager: npm
  • Formatter/Linter: Biome (npm run check)
  • Tests: Vitest (npm test)
  • GraphQL codegen: npm run generate (never edit src/gql/)

Audience

This file is for AI coding agents working on the Linearis codebase. For user documentation, see README.md. For human contributor guidelines, see CONTRIBUTING.md.

Quick Commands

npm install          # install deps, codegen, and lefthook (via prepare hook)
npm test             # unit tests (vitest)
npm run build        # compile to dist/
npm run generate     # regenerate GraphQL types from .graphql files
npm run check        # biome format + lint (auto-fix)
npm run check:ci     # biome format + lint (CI, no fix)
npm run generate:usage  # regenerate USAGE.md

Commit Rules

  • Conventional Commits required (enforced by commitlint via lefthook).
  • Issue/PR refs in commit body when applicable: Closes #n, Refs #n, Part of #n.
  • No AI co-author trailers. Do not add Co-authored-by for AI assistants.

See CONTRIBUTING.md for the full commit types table and examples.

Architecture (5 Layers)

CLI Input → Command → Resolver → Service → JSON Output
               │         │          │
           createContext GraphQL  GraphQL
                        (UUID)   (data)
LayerDirectoryClientResponsibility
Clientsrc/client/Thin API wrappers, no logic
Resolversrc/resolvers/GraphQLClientHuman ID → UUID conversion
Servicesrc/services/GraphQLClientBusiness logic, CRUD via GraphQL
Commandsrc/commands/GraphQLClient via createContext()CLI orchestration only
Commonsrc/common/Shared types, errors, output, auth

Invariants (P0 — violations fail CI/review)

  1. No any types. Use unknown, codegen types, or explicit interfaces.
  2. Strict layer separation. No cross-layer imports:
    • Resolvers must not import services (or vice versa).
    • Commands must not construct GraphQLClient directly — use createContext().
  3. Client-layer contract:
    • Resolvers → GraphQLClient (via lean filter-based lookup queries).
    • Services → GraphQLClient.
    • Commands → GraphQLClient via createContext() (ctx.gql).
    • Resolvers should prefer the lean lookup fragments; when the Linear API exposes no lean lookup for an entity, a resolver may query it directly with an explicit ARCHITECTURAL EXCEPTION docstring (current examples: milestone/project-status lookups, initiative relation/link ID lookup helpers).
  4. ID resolution happens once, in resolvers only. Services accept UUIDs.
  5. All commands use handleCommand() wrapper and outputSuccess() for output.
  6. Explicit return types on all exported functions.
  7. ES module imports use .js extensions (even for .ts files).
  8. Never edit src/gql/ — it is generated by codegen.
  9. USAGE.md is generated and git-ignored — produced by npm run generate:usage during build, included in the npm package but not committed.
  10. Production dependencies must be pinned to exact versions (no ^ or ~). Dev dependencies may use ranges.
  11. No postinstall scripts. Use prepare for dev setup hooks (codegen, lefthook). Consumer installs must never execute dev-only commands.

Decision Tree: Adding Functionality

Need a new GraphQL operation?
  → Add/edit graphql/{queries,mutations}/*.graphql
  → Run: npm run generate
  → Import DocumentNode + types from src/gql/graphql.js

Need to resolve a human-friendly ID?
  → Add/edit src/resolvers/*-resolver.ts
  → Use GraphQLClient with a lean lookup query, return UUID string
  → Pattern: UUID passthrough → GraphQL filter lookup → notFoundError()
  → If no lean lookup fragment exists, query directly as a documented ARCHITECTURAL EXCEPTION (include rationale in resolver docstring)

Need business logic / CRUD?
  → Add/edit src/services/*-service.ts
  → Use GraphQLClient, accept pre-resolved UUIDs only
  → Import codegen DocumentNode + types

Need a CLI command?
  → Add/edit src/commands/*.ts
  → Use createContext() → resolve IDs → call service → outputSuccess()
  → Register in src/main.ts (setupXCommands + META in allMetas[])
  → Add DomainMeta export + usage subcommand

Need tests?
  → Add tests/unit/{resolvers,services,common}/*.test.ts
  → Mock ONE layer deep (see Testing section below)

Testing

Tests mirror src/ structure under tests/unit/. Mock the dependency one layer down:

Test targetMockExample
ResolverGraphQLClient (mock request){ request: vi.fn() } as unknown as GraphQLClient
ServiceGraphQLClient (mock request){ request: vi.fn() } as unknown as GraphQLClient
CommonNo mocks (pure functions)Direct import + assert

Coverage minimum: happy path + primary error case per function.

Integration tests (tests/integration/) need LINEAR_API_TOKEN set and a built project. They are skipped automatically when the token is absent.

npm test                        # unit tests
npx vitest run tests/unit/resolvers  # specific suite
npm run test:coverage           # coverage report

Anti-Patterns (WRONG → RIGHT)

ID resolution in service:

// WRONG: service resolves IDs
async function createIssue(client: GraphQLClient, teamName: string) {
  const teamId = await resolveTeamId(...); // ← not here
}
// RIGHT: service receives UUID
async function createIssue(client: GraphQLClient, input: { teamId: string }) { ... }

ID resolution in command:

// WRONG: command builds a raw mutation instead of delegating
async function resolveTeamId(client: GraphQLClient, teamName: string) {
  const teamId = /* inline lookup in the command */;
}
// RIGHT: resolvers own ID resolution, services own CRUD
async function resolveTeamId(client: GraphQLClient, teamName: string): Promise<UUID> { ... }

Business logic in command:

// WRONG: command does data work
.action(handleCommand(async (title, opts) => {
  const result = await ctx.gql.request(SomeMutation, { title, teamId: opts.team });
}))
// RIGHT: command delegates
.action(handleCommand(async (title, opts) => {
  const teamId = await resolveTeamId(ctx.gql, opts.team);
  const result = await createIssue(ctx.gql, { title, teamId });
  outputSuccess(result);
}))

GraphQL Workflow

1. Edit:   graphql/{queries,mutations}/*.graphql
2. Run:    npm run generate
3. Import: import { FooDocument, type FooQuery } from "../gql/graphql.js"
4. Use:    client.request<FooQuery>(FooDocument, variables)

Never use raw GraphQL strings. Always type client.request<T>().

Usage Documentation System

Every command group must export a DomainMeta and register a usage subcommand. See src/common/usage.ts for the DomainMeta interface and formatting functions.

export const ENTITY_META: DomainMeta = {
  name: "entity",
  summary: "short one-line description",
  context: "data model explanation",
  arguments: { name: "description" },
  seeAlso: ["related-domain command"],
};

Registration checklist:

  1. Export DOMAIN_META from command file.
  2. Add usage subcommand: .command("usage").action(() => console.log(formatDomainUsage(...))).
  3. Add meta to allMetas[] in src/main.ts.
  4. Run npm run generate:usage to update USAGE.md.

File Map

src/
  main.ts              # entry point, command registration
  client/              # GraphQLClient
  resolvers/           # ID resolution (human → UUID)
  services/            # business logic (GraphQL CRUD)
  commands/            # CLI definitions (Commander.js)
  common/              # context, output, errors, types, auth, usage
  gql/                 # GENERATED — do not edit
graphql/
  queries/             # .graphql query definitions
  mutations/           # .graphql mutation definitions
tests/
  unit/                # mirrors src/ structure
  integration/         # CLI integration tests (need API token)

Verification Checklist

Before claiming work is complete, run:

npm run check:ci       # biome lint + format check
npx tsc --noEmit       # type check
npm test               # unit tests
npm run build          # full build (includes codegen + usage generation)
npm run knip           # dead-code check (unused files/exports/types/deps)

All five must pass. CI runs the first four on every push and PR; the knip check runs on PRs only, where it is required — it posts a self-updating comment listing any dead code, and npx knip --fix --allow-remove-files auto-removes most findings. Run npm run generate (or npm run build) first so generated GraphQL types exist. Genuine false positives (dynamically-wired code knip cannot trace) are suppressed in knip.json, not left unaddressed.

Extended Documentation

For deeper patterns, templates, and implementation details, see:

  • docs/architecture.md — component organization, data flow, key files
  • docs/development.md — code patterns, service/resolver/command templates
  • docs/testing.md — mock patterns, writing new tests, integration tests
  • docs/build-system.md — compilation, codegen pipeline
  • docs/files.md — complete file catalog