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 editsrc/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-byfor 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)
| Layer | Directory | Client | Responsibility |
|---|---|---|---|
| Client | src/client/ | — | Thin API wrappers, no logic |
| Resolver | src/resolvers/ | GraphQLClient | Human ID → UUID conversion |
| Service | src/services/ | GraphQLClient | Business logic, CRUD via GraphQL |
| Command | src/commands/ | GraphQLClient via createContext() | CLI orchestration only |
| Common | src/common/ | — | Shared types, errors, output, auth |
Invariants (P0 — violations fail CI/review)
- No
anytypes. Useunknown, codegen types, or explicit interfaces. - Strict layer separation. No cross-layer imports:
- Resolvers must not import services (or vice versa).
- Commands must not construct
GraphQLClientdirectly — usecreateContext().
- Client-layer contract:
- Resolvers →
GraphQLClient(via lean filter-based lookup queries). - Services →
GraphQLClient. - Commands →
GraphQLClientviacreateContext()(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 EXCEPTIONdocstring (current examples: milestone/project-status lookups, initiative relation/link ID lookup helpers).
- Resolvers →
- ID resolution happens once, in resolvers only. Services accept UUIDs.
- All commands use
handleCommand()wrapper andoutputSuccess()for output. - Explicit return types on all exported functions.
- ES module imports use
.jsextensions (even for.tsfiles). - Never edit
src/gql/— it is generated by codegen. USAGE.mdis generated and git-ignored — produced bynpm run generate:usageduring build, included in the npm package but not committed.- Production dependencies must be pinned to exact versions (no
^or~). Dev dependencies may use ranges. - No
postinstallscripts. Usepreparefor 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 target | Mock | Example |
|---|---|---|
| Resolver | GraphQLClient (mock request) | { request: vi.fn() } as unknown as GraphQLClient |
| Service | GraphQLClient (mock request) | { request: vi.fn() } as unknown as GraphQLClient |
| Common | No 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:
- Export
DOMAIN_METAfrom command file. - Add
usagesubcommand:.command("usage").action(() => console.log(formatDomainUsage(...))). - Add meta to
allMetas[]insrc/main.ts. - Run
npm run generate:usageto updateUSAGE.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 filesdocs/development.md— code patterns, service/resolver/command templatesdocs/testing.md— mock patterns, writing new tests, integration testsdocs/build-system.md— compilation, codegen pipelinedocs/files.md— complete file catalog